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) <noreply@anthropic.com>
This commit is contained in:
1 parent
94eeb1d817
commit
8625d85a87
11 files changed
+145
No files matched your search
@@ -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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/** @type {import('eslint').Linter.Config} */
|
||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ['@ketopath/eslint-config/node.cjs'],
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
dist/
|
||||||
|
*.tsbuildinfo
|
||||||
Whitespace-only changes.
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<FastifyInstance> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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<typeof envSchema>;
|
||||||
|
|
||||||
|
export const env: Env = envSchema.parse(process.env);
|
||||||
@@ -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(),
|
||||||
|
}));
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { buildApp } from './app.js';
|
||||||
|
import { env } from './config/env.js';
|
||||||
|
|
||||||
|
async function start(): Promise<void> {
|
||||||
|
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();
|
||||||
@@ -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"]
|
||||||
|
}
|
||||||
Generated
BIN
Binary file not shown.
Reference in new issue
Block a user