import cookie from '@fastify/cookie'; import cors from '@fastify/cors'; import rateLimit from '@fastify/rate-limit'; import websocket from '@fastify/websocket'; import Fastify from 'fastify'; import { ZodError } from 'zod'; import { sql } from './db'; import { env } from './env'; import { loadUser } from './lib/auth'; import { HttpError } from './lib/http'; import { redis } from './lib/redis'; import { authRoutes } from './routes/auth'; import { connectionRoutes } from './routes/connections'; import { deviceRoutes } from './routes/devices'; import { meRoutes } from './routes/me'; import { realtimeRoutes } from './routes/realtime'; import { trackRoutes } from './routes/tracks'; import { userRoutes } from './routes/users'; import { webhookRoutes } from './routes/webhooks'; /** Builds the API without listening, so tests can drive it with app.inject(). */ export async function buildApp({ logger = true, rateLimits = true }: { logger?: boolean; rateLimits?: boolean } = {}) { const app = Fastify({ logger, trustProxy: true }); await app.register(cors, { origin: [env.WEB_URL], credentials: true, methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], }); await app.register(cookie); // Rate limits are per-route (see `config.rateLimit`); the Redis store makes them global across instances. if (rateLimits) await app.register(rateLimit, { global: false, redis }); await app.register(websocket); app.decorateRequest('user', null); app.decorateRequest('sessionId', null); app.addHook('onRequest', loadUser); app.setErrorHandler((err, req, reply) => { if (err instanceof ZodError) { return reply.status(400).send({ error: err.issues[0]?.message ?? 'Invalid input', issues: err.issues }); } if (err instanceof HttpError) return reply.status(err.status).send({ error: err.message }); const status = (err as { statusCode?: number }).statusCode; if (status && status < 500) return reply.status(status).send({ error: (err as Error).message }); req.log.error(err); return reply.status(500).send({ error: 'Internal server error' }); }); app.get('/health', async () => { await Promise.all([sql`select 1`, redis.ping()]); return { ok: true }; }); await app.register(authRoutes); await app.register(meRoutes); await app.register(userRoutes); await app.register(trackRoutes); await app.register(connectionRoutes); await app.register(deviceRoutes); await app.register(realtimeRoutes); // registered as its own plugin so its permissive body parsers stay scoped to webhook routes await app.register(webhookRoutes); return app; }