tercul-frontend/server/index.ts
google-labs-jules[bot] a50e42094a Implement advanced search filters UI and backend support
- Add Author, Language, Work Type, Date Range filters
- Implement Save Search Preferences
- Add backend routes for search and filtering with client-side pagination support
2025-12-01 09:22:40 +00:00

109 lines
3.1 KiB
TypeScript

import express, {
type NextFunction,
type Request,
type Response,
} from "express";
import { attachGraphQLClient } from "./middleware/graphql";
import commentRouter from "./routes/comment";
import userRouter from "./routes/user";
import authorRouter from "./routes/author";
import workRouter from "./routes/work";
import tagRouter from "./routes/tag";
import collectionRouter from "./routes/collection";
import blogRouter from "./routes/blog";
import statsRouter from "./routes/stats";
import searchRouter from "./routes/search";
import filterRouter from "./routes/filter";
import { log, serveStatic, setupVite } from "./vite";
import { createServer } from "node:http";
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// Attach per-request GraphQL client
app.use(attachGraphQLClient);
app.use((req, res, next) => {
const start = Date.now();
const path = req.path;
let capturedJsonResponse: unknown;
const originalResJson = res.json;
res.json = (bodyJson, ...args) => {
capturedJsonResponse = bodyJson;
return originalResJson.apply(res, [bodyJson, ...args]);
};
res.on("finish", () => {
const duration = Date.now() - start;
if (path.startsWith("/api")) {
let logLine = `${req.method} ${path} ${res.statusCode} in ${duration}ms`;
if (capturedJsonResponse) {
logLine += ` :: ${JSON.stringify(capturedJsonResponse)}`;
}
if (logLine.length > 80) {
logLine = `${logLine.slice(0, 79)}`;
}
log(logLine);
}
});
next();
});
(async () => {
// Domain routers (incremental migration from monolith)
app.use("/api/users", userRouter);
app.use("/api/authors", authorRouter);
app.use("/api/works", workRouter);
app.use("/api/tags", tagRouter);
app.use("/api/comments", commentRouter);
app.use("/api/collections", collectionRouter);
app.use("/api/blog", blogRouter);
app.use("/api/stats", statsRouter);
app.use("/api/search", searchRouter);
app.use("/api/filter", filterRouter);
// comments router already mounted earlier at /api/comments
const server = createServer(app);
app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => {
const fallbackStatus = 500;
interface StatusCarrier {
status?: number;
statusCode?: number;
}
const status =
typeof err === "object" && err
? ((err as StatusCarrier).status ??
(err as StatusCarrier).statusCode ??
fallbackStatus)
: fallbackStatus;
const message =
err instanceof Error ? err.message : "Internal Server Error";
res.status(status).json({ message });
throw err;
});
// importantly only setup vite in development and after
// setting up all the other routes so the catch-all route
// doesn't interfere with the other routes
if (app.get("env") === "development") {
await setupVite(app, server);
} else {
serveStatic(app);
}
// ALWAYS serve the app on port 5000
// this serves both the API and the client.
// It is the only port that is not firewalled.
const port = 5000;
server.listen(port, "0.0.0.0", () => {
log(`serving on port ${port}`);
});
})();