From 8625d85a876ce0750e9cc8df2f9c0c653dfec652 Mon Sep 17 00:00:00 2001 From: luciano Date: Wed, 29 Apr 2026 12:19:04 +0200 Subject: [PATCH] feat(api): scaffold Fastify server with health route - ESM Fastify 4 with Pino logger (pretty-print in dev only) - Plugins: helmet, cors (allowlist via CORS_ORIGINS), rate-limit, sensible - Zod-validated environment config in src/config/env.ts - Modular structure under src/modules with /health route - tsx watch for dev, tsc for build, dist/server.js as production entry Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/api/.env.example | 10 +++++ apps/api/.eslintrc.cjs | 5 +++ apps/api/.gitignore | 2 + apps/api/.gitkeep | 0 apps/api/package.json | 29 ++++++++++++++ apps/api/src/app.ts | 40 +++++++++++++++++++ apps/api/src/config/env.ts | 21 ++++++++++ apps/api/src/modules/health/health.routes.ts | 9 +++++ apps/api/src/server.ts | 15 +++++++ apps/api/tsconfig.json | 14 +++++++ pnpm-lock.yaml | Bin 138215 -> 166996 bytes 11 files changed, 145 insertions(+) create mode 100644 apps/api/.env.example create mode 100644 apps/api/.eslintrc.cjs create mode 100644 apps/api/.gitignore delete mode 100644 apps/api/.gitkeep create mode 100644 apps/api/package.json create mode 100644 apps/api/src/app.ts create mode 100644 apps/api/src/config/env.ts create mode 100644 apps/api/src/modules/health/health.routes.ts create mode 100644 apps/api/src/server.ts create mode 100644 apps/api/tsconfig.json diff --git a/apps/api/.env.example b/apps/api/.env.example new file mode 100644 index 0000000..8789db7 --- /dev/null +++ b/apps/api/.env.example @@ -0,0 +1,10 @@ +NODE_ENV=development +PORT=4000 +HOST=127.0.0.1 +LOG_LEVEL=info + +# CORS — origini consentite (separate da virgola) +CORS_ORIGINS=http://localhost:3000 + +# Database (configurato in step successivo con Prisma) +# DATABASE_URL=postgresql://user:password@localhost:5432/ketopath diff --git a/apps/api/.eslintrc.cjs b/apps/api/.eslintrc.cjs new file mode 100644 index 0000000..2ad14cf --- /dev/null +++ b/apps/api/.eslintrc.cjs @@ -0,0 +1,5 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + extends: ['@ketopath/eslint-config/node.cjs'], +}; diff --git a/apps/api/.gitignore b/apps/api/.gitignore new file mode 100644 index 0000000..4bf7b95 --- /dev/null +++ b/apps/api/.gitignore @@ -0,0 +1,2 @@ +dist/ +*.tsbuildinfo diff --git a/apps/api/.gitkeep b/apps/api/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..fb34133 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,29 @@ +{ + "name": "@ketopath/api", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/server.ts", + "build": "tsc", + "start": "node dist/server.js", + "typecheck": "tsc --noEmit", + "lint": "eslint src --max-warnings=0" + }, + "dependencies": { + "@fastify/cors": "^9.0.1", + "@fastify/helmet": "^11.1.1", + "@fastify/rate-limit": "^9.1.0", + "@fastify/sensible": "^5.6.0", + "@ketopath/shared": "workspace:*", + "fastify": "^4.28.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@ketopath/eslint-config": "workspace:*", + "@ketopath/tsconfig": "workspace:*", + "@types/node": "^20.12.7", + "pino-pretty": "^11.2.1", + "tsx": "^4.16.2" + } +} diff --git a/apps/api/src/app.ts b/apps/api/src/app.ts new file mode 100644 index 0000000..61dd6f5 --- /dev/null +++ b/apps/api/src/app.ts @@ -0,0 +1,40 @@ +import cors from '@fastify/cors'; +import helmet from '@fastify/helmet'; +import rateLimit from '@fastify/rate-limit'; +import sensible from '@fastify/sensible'; +import Fastify, { type FastifyInstance } from 'fastify'; + +import { env } from './config/env.js'; +import { healthRoutes } from './modules/health/health.routes.js'; + +export async function buildApp(): Promise { + const app = Fastify({ + logger: { + level: env.LOG_LEVEL, + ...(env.NODE_ENV === 'development' + ? { + transport: { + target: 'pino-pretty', + options: { translateTime: 'HH:MM:ss', ignore: 'pid,hostname' }, + }, + } + : {}), + }, + disableRequestLogging: env.NODE_ENV === 'production', + }); + + await app.register(helmet, { global: true }); + await app.register(cors, { + origin: env.CORS_ORIGINS, + credentials: true, + }); + await app.register(rateLimit, { + max: 100, + timeWindow: '1 minute', + }); + await app.register(sensible); + + await app.register(healthRoutes); + + return app; +} diff --git a/apps/api/src/config/env.ts b/apps/api/src/config/env.ts new file mode 100644 index 0000000..f496234 --- /dev/null +++ b/apps/api/src/config/env.ts @@ -0,0 +1,21 @@ +import { z } from 'zod'; + +const envSchema = z.object({ + NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), + PORT: z.coerce.number().int().positive().default(4000), + HOST: z.string().min(1).default('127.0.0.1'), + LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace', 'silent']).default('info'), + CORS_ORIGINS: z + .string() + .default('http://localhost:3000') + .transform((value) => + value + .split(',') + .map((origin) => origin.trim()) + .filter(Boolean), + ), +}); + +export type Env = z.infer; + +export const env: Env = envSchema.parse(process.env); diff --git a/apps/api/src/modules/health/health.routes.ts b/apps/api/src/modules/health/health.routes.ts new file mode 100644 index 0000000..563eb03 --- /dev/null +++ b/apps/api/src/modules/health/health.routes.ts @@ -0,0 +1,9 @@ +import type { FastifyPluginAsync } from 'fastify'; + +export const healthRoutes: FastifyPluginAsync = async (fastify) => { + fastify.get('/health', async () => ({ + status: 'ok', + uptime: process.uptime(), + timestamp: new Date().toISOString(), + })); +}; diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts new file mode 100644 index 0000000..ebcc6f9 --- /dev/null +++ b/apps/api/src/server.ts @@ -0,0 +1,15 @@ +import { buildApp } from './app.js'; +import { env } from './config/env.js'; + +async function start(): Promise { + const app = await buildApp(); + + try { + await app.listen({ port: env.PORT, host: env.HOST }); + } catch (err) { + app.log.error(err); + process.exit(1); + } +} + +void start(); diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..ee8203d --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "@ketopath/tsconfig/node.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "types": ["node"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c085e367474da83a98980330b44153279e8765a..7ce307642b3f900d209f399dd8f2a490766d8f67 100644 GIT binary patch delta 20540 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 delta 964 zcmWmCe@xV690%}yKHvM^z8v@CE5F>~UC@Pzz|I|v+QACO22z%p%XX5cukuey0pH7{zj5bHLRl za8J`?wUxn;xDs2%Go`S>Jf4_^F&itk5)^fVCbj>ndUzMW!;ACK0UW&tQy}yalTX}- z0tbJJSm9NW9{O(u8oa`XPN052&RdLr-VneX5NEEsdHOMIR*zqaY*0FgL3Xdf0E#ac z3n~6I=22}IuAy7I)CkUpu+(8$c3X-bO0Pv1D@p@7^(?-tky2bnmFqD|i|g@s_CAN{ zEK+*Mato(e-C0IC0kYFWvzJ0@~xri zq#D5OHoO3QVFx;qZ@r2QDkusC^tpBQTn8?v)NVXrN#|2$R?S9bET@SuR~XoPC|}#P);xOs_pbDw9ywyEMr#^QwFQ=@`?UG8f9ys