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 3c085e3..7ce3076 100644 Binary files a/pnpm-lock.yaml and b/pnpm-lock.yaml differ