feat(backend): bootstrap express app with error handling

This commit is contained in:
2026-05-20 20:36:55 +02:00
parent 59261b3bab
commit d13af79940
7 changed files with 5070 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
import express, { type Express, type NextFunction, type Request, type Response } from 'express';
import { ZodError } from 'zod';
import { ApiError } from './lib/errors.js';
export function createApp(): Express {
const app = express();
app.use(express.json({ limit: '5mb' }));
app.get('/api/health', (_req, res) => {
res.json({ ok: true });
});
// Routes mounted in later tasks.
app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => {
if (err instanceof ZodError) {
res.status(400).json({
error: { code: 'VALIDATION_ERROR', message: 'Invalid input', details: err.flatten() },
});
return;
}
if (err instanceof ApiError) {
res.status(err.status).json({
error: { code: err.code, message: err.message, details: err.details },
});
return;
}
console.error(err);
res.status(500).json({ error: { code: 'INTERNAL', message: 'Internal server error' } });
});
return app;
}

View File

@@ -0,0 +1,7 @@
import { createApp } from './app.js';
const PORT = Number(process.env.PORT ?? 3000);
const app = createApp();
app.listen(PORT, () => {
console.log(`Backend listening on http://localhost:${PORT}`);
});

View File

@@ -0,0 +1,20 @@
export class ApiError extends Error {
constructor(
public status: number,
public code: string,
message: string,
public details?: unknown
) {
super(message);
}
static notFound(what = 'Resource') {
return new ApiError(404, 'NOT_FOUND', `${what} not found`);
}
static validation(message: string, details?: unknown) {
return new ApiError(400, 'VALIDATION_ERROR', message, details);
}
static conflict(message: string) {
return new ApiError(409, 'CONFLICT', message);
}
}