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

4952
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,39 @@
{
"name": "@flashcard/backend",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc -p tsconfig.json",
"start": "node dist/index.js",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"db:generate": "drizzle-kit generate",
"db:migrate": "tsx src/db/migrate.ts",
"db:seed": "tsx src/db/seed.ts"
},
"dependencies": {
"@flashcard/shared": "*",
"better-sqlite3": "^11.0.0",
"drizzle-orm": "^0.33.0",
"express": "^4.19.0",
"multer": "^1.4.5-lts.1",
"xlsx": "^0.18.5",
"zod": "^3.23.0"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.0",
"@types/express": "^4.17.0",
"@types/multer": "^1.4.0",
"@types/node": "^20.0.0",
"@types/supertest": "^6.0.0",
"drizzle-kit": "^0.24.0",
"supertest": "^7.0.0",
"tsx": "^4.16.0",
"typescript": "^5.5.0",
"vitest": "^2.0.0"
}
}

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);
}
}

View File

@@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"module": "NodeNext",
"moduleResolution": "NodeNext"
},
"include": ["src/**/*"]
}

View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
include: ['src/**/*.test.ts'],
pool: 'forks',
},
});