feat: refactored /api into its own router with error handler
Build and Deploy / Build and Upload (push) Successful in 1m33s
Build and Deploy / Upload (push) Successful in 1m48s

This commit is contained in:
redglow
2026-06-24 10:09:48 +02:00
parent 463943abab
commit 1a7deed5b0
3 changed files with 147 additions and 133 deletions
+142
View File
@@ -0,0 +1,142 @@
import { unlink } from "node:fs/promises";
import path from "node:path";
import { NextFunction, Request, Response, Router } from "express";
import multer from "multer";
import z from "zod";
import { baseUploadsFolder } from "./base-uploads-folder.ts";
import { db } from "./db/database.ts";
const PostInput = z.object({
file: z.object({ filename: z.string() }),
body: z.object({
publication_date: z.coerce
.date()
.transform((x) => new Date(x).toISOString()),
title: z.string().nonempty(),
text: z.string().nonempty(),
}),
});
const PatchInput = z.object({
file: z.object({ filename: z.string().optional() }).optional(),
body: z.object({
publication_date: z.coerce
.date()
.optional()
.transform((x) => (x ? new Date(x).toISOString() : x)),
title: z.string().nonempty().optional(),
text: z.string().nonempty().optional(),
}),
});
const AnnouncementIdInput = z.object({
announcementId: z.coerce.number().positive(),
});
const GetFromGame = z.object({
gameName: z.string().nonempty(),
});
const upload = multer({
dest: baseUploadsFolder,
});
export const apiRouter = Router();
apiRouter.get("/announcements", async (_, res) => {
console.log(`Processing GET /api/announcements`);
const announcements = await db
.selectFrom("announcements")
.select(["id", "title", "text", "publication_date", "image"])
.orderBy("announcements.publication_date", "desc")
.execute();
res.json(announcements);
});
apiRouter.get("/announcements/from-game", async (req, res) => {
console.log(`Processing GET /api/announcements/from-game`);
const fromGameData = GetFromGame.parse(req.query);
console.log(`Request from game ${fromGameData.gameName}`);
const announcements = await db
.selectFrom("announcements")
.select(["id", "title", "text", "publication_date", "image"])
.orderBy("announcements.publication_date", "desc")
.where("publication_date", "<=", new Date().toISOString())
.limit(3)
.execute();
res.json(announcements);
});
apiRouter.get("/announcements/:announcementId", async (req, res) => {
console.log(`Processing GET /api/announcements/:announcementId`);
const { announcementId } = AnnouncementIdInput.parse(req.params);
const announcement = await db
.selectFrom("announcements")
.select(["id", "title", "text", "publication_date", "image"])
.where("id", "=", announcementId)
.executeTakeFirstOrThrow();
res.json(announcement);
});
apiRouter.post("/announcements", upload.single("image"), async (req, res) => {
console.log(`Processing POST /api/announcements`);
const postInput = PostInput.parse(req);
const announcement = {
title: postInput.body.title,
text: postInput.body.text,
publication_date: postInput.body.publication_date,
image: postInput.file.filename,
};
await db.insertInto("announcements").values(announcement).execute();
res.redirect("/");
});
apiRouter.post(
"/announcements/:announcementId",
upload.single("image"),
async (req, res) => {
console.log(`Processing POST /api/announcements/:announcementId`);
const patchInput = PatchInput.parse(req);
const { announcementId } = AnnouncementIdInput.parse(req.params);
await db
.updateTable("announcements")
.set({
title: patchInput.body.title,
text: patchInput.body.text,
publication_date: patchInput.body.publication_date,
image: patchInput.file?.filename,
})
.where("id", "=", announcementId)
.execute();
res.redirect("/");
},
);
apiRouter.delete("/announcements/:announcementId", async (req, res) => {
console.log(`Processing DELETE /api/announcements`);
const { announcementId } = AnnouncementIdInput.parse(req.params);
const { image } = await db
.selectFrom("announcements")
.select("image")
.where("id", "=", announcementId)
.executeTakeFirstOrThrow();
await db
.deleteFrom("announcements")
.where("id", "=", announcementId)
.execute();
await unlink(path.join(baseUploadsFolder, image));
res.status(204).send();
});
apiRouter.use(
(_err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error(_err);
res.status(500).send("Internal Server Error.");
},
);
+2
View File
@@ -0,0 +1,2 @@
export const baseUploadsFolder = process.env.UPLOADS_DIR ?? "uploads/";
console.log(`Using ${baseUploadsFolder} as base uploads folder`);
+3 -133
View File
@@ -1,146 +1,16 @@
import { unlink } from "node:fs/promises";
import path from "node:path";
import express from "express"; import express from "express";
import multer from "multer";
import ViteExpress from "vite-express"; import ViteExpress from "vite-express";
import z from "zod"; import { apiRouter } from "./api.ts";
import { db } from "./db/database.ts"; import { baseUploadsFolder } from "./base-uploads-folder.ts";
const baseUploadsFolder = process.env.UPLOADS_DIR ?? "uploads/";
console.log(`Using ${baseUploadsFolder} as base uploads folder`);
const upload = multer({
dest: baseUploadsFolder,
});
const app = express(); const app = express();
const PostInput = z.object({
file: z.object({ filename: z.string() }),
body: z.object({
publication_date: z.coerce
.date()
.transform((x) => new Date(x).toISOString()),
title: z.string().nonempty(),
text: z.string().nonempty(),
}),
});
const PatchInput = z.object({
file: z.object({ filename: z.string().optional() }).optional(),
body: z.object({
publication_date: z.coerce
.date()
.optional()
.transform((x) => (x ? new Date(x).toISOString() : x)),
title: z.string().nonempty().optional(),
text: z.string().nonempty().optional(),
}),
});
const AnnouncementIdInput = z.object({
announcementId: z.coerce.number().positive(),
});
const GetFromGame = z.object({
gameName: z.string().nonempty(),
});
app.use((req, _res, next) => { app.use((req, _res, next) => {
console.log(`Received request ${req.url}`); console.log(`Received request ${req.url}`);
next(); next();
}); });
app.get("/api/announcements", async (_, res) => { app.use("/api", apiRouter);
console.log(`Processing GET /api/announcements`);
const announcements = await db
.selectFrom("announcements")
.select(["id", "title", "text", "publication_date", "image"])
.orderBy("announcements.publication_date", "desc")
.execute();
res.json(announcements);
});
app.get("/api/announcements/from-game", async (req, res) => {
console.log(`Processing GET /api/announcements/from-game`);
const fromGameData = GetFromGame.parse(req.query);
console.log(`Request from game ${fromGameData.gameName}`);
const announcements = await db
.selectFrom("announcements")
.select(["id", "title", "text", "publication_date", "image"])
.orderBy("announcements.publication_date", "desc")
.where("publication_date", "<=", new Date().toISOString())
.limit(3)
.execute();
res.json(announcements);
});
app.get("/api/announcements/:announcementId", async (req, res) => {
console.log(`Processing GET /api/announcements/:announcementId`);
const { announcementId } = AnnouncementIdInput.parse(req.params);
const announcement = await db
.selectFrom("announcements")
.select(["id", "title", "text", "publication_date", "image"])
.where("id", "=", announcementId)
.executeTakeFirstOrThrow();
res.json(announcement);
});
app.post("/api/announcements", upload.single("image"), async (req, res) => {
console.log(`Processing POST /api/announcements`);
const postInput = PostInput.parse(req);
const announcement = {
title: postInput.body.title,
text: postInput.body.text,
publication_date: postInput.body.publication_date,
image: postInput.file.filename,
};
await db.insertInto("announcements").values(announcement).execute();
res.redirect("/");
});
app.post(
"/api/announcements/:announcementId",
upload.single("image"),
async (req, res) => {
console.log(`Processing POST /api/announcements/:announcementId`);
const patchInput = PatchInput.parse(req);
const { announcementId } = AnnouncementIdInput.parse(req.params);
await db
.updateTable("announcements")
.set({
title: patchInput.body.title,
text: patchInput.body.text,
publication_date: patchInput.body.publication_date,
image: patchInput.file?.filename,
})
.where("id", "=", announcementId)
.execute();
res.redirect("/");
},
);
app.delete("/api/announcements/:announcementId", async (req, res) => {
console.log(`Processing DELETE /api/announcements`);
const { announcementId } = AnnouncementIdInput.parse(req.params);
const { image } = await db
.selectFrom("announcements")
.select("image")
.where("id", "=", announcementId)
.executeTakeFirstOrThrow();
await db
.deleteFrom("announcements")
.where("id", "=", announcementId)
.execute();
await unlink(path.join(baseUploadsFolder, image));
res.status(204).send();
});
app.use("/images", express.static(baseUploadsFolder)); app.use("/images", express.static(baseUploadsFolder));