backend-node/fastify-project-starter/SKILL.md
Scaffold a production-ready Fastify 5.x API with TypeScript, plugin architecture, JSON Schema validation, and auto-generated Swagger docs.
npx skillsauth add achreftlili/deep-dev-skills fastify-project-starterInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Scaffold a production-ready Fastify 5.x API with TypeScript, plugin architecture, JSON Schema validation, and auto-generated Swagger docs.
mkdir <project-name> && cd <project-name>
pnpm init
pnpm add fastify @fastify/autoload @fastify/sensible @fastify/cors @fastify/helmet @fastify/swagger @fastify/swagger-ui @fastify/env @fastify/type-provider-typebox @sinclair/typebox
pnpm add -D typescript @types/node tsx tsup
# Generate tsconfig
npx tsc --init --strict --target ES2022 --module NodeNext \
--moduleResolution NodeNext --outDir dist --rootDir src \
--esModuleInterop --skipLibCheck --forceConsistentCasingInFileNames
mkdir -p src/{plugins,routes,services,schemas,types}
src/
server.ts # Entry point — builds and starts the server
app.ts # App factory — registers plugins and routes
plugins/
sensible.ts # @fastify/sensible — httpErrors, to, assert
cors.ts # CORS configuration
swagger.ts # Swagger + Swagger UI setup
env.ts # Typed env config via @fastify/env
auth.ts # Auth decorator (e.g. JWT verification)
routes/
users/
index.ts # Auto-loaded route plugin — GET/POST/PATCH/DELETE
schema.ts # TypeBox schemas for this resource
health/
index.ts # Health check endpoint
services/
users.service.ts # Business logic — injected via decorators or DI
schemas/
shared.ts # Reusable schema fragments (pagination, errors)
types/
index.d.ts # Module augmentation for Fastify instance
.env.example # Required env vars template
fastify.register().@fastify/autoload to auto-register all plugins and route files by directory convention.fastify-plugin (fp) only when a plugin must be visible to sibling and parent contexts.fast-json-stringify for 2-3x faster serialization.any — leverage @fastify/type-provider-typebox for end-to-end type safety from schema to handler.app.tsimport Fastify from 'fastify';
import autoload from '@fastify/autoload';
import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = fileURLToPath(new URL('.', import.meta.url));
export async function buildApp() {
const app = Fastify({
logger: {
level: process.env.LOG_LEVEL ?? 'info',
},
}).withTypeProvider<TypeBoxTypeProvider>();
// Register plugins (cors, helmet, swagger, env, etc.)
await app.register(autoload, {
dir: join(__dirname, 'plugins'),
forceESM: true,
});
// Register routes
await app.register(autoload, {
dir: join(__dirname, 'routes'),
options: { prefix: '/api' },
forceESM: true,
});
return app;
}
server.tsimport { buildApp } from './app.js';
const app = await buildApp();
try {
await app.listen({ port: Number(process.env.PORT ?? 3000), host: '0.0.0.0' });
} catch (err) {
app.log.error(err);
process.exit(1);
}
plugins/env.tsimport fp from 'fastify-plugin';
import fastifyEnv from '@fastify/env';
const schema = {
type: 'object' as const,
required: ['DATABASE_URL'],
properties: {
PORT: { type: 'number', default: 3000 },
NODE_ENV: { type: 'string', default: 'development' },
DATABASE_URL: { type: 'string' },
JWT_SECRET: { type: 'string' },
},
};
export default fp(async (fastify) => {
await fastify.register(fastifyEnv, { schema, dotenv: true });
});
// Augment Fastify types — src/types/index.d.ts
// declare module 'fastify' {
// interface FastifyInstance {
// config: { PORT: number; NODE_ENV: string; DATABASE_URL: string; JWT_SECRET: string };
// }
// }
plugins/swagger.tsimport fp from 'fastify-plugin';
import swagger from '@fastify/swagger';
import swaggerUi from '@fastify/swagger-ui';
export default fp(async (fastify) => {
await fastify.register(swagger, {
openapi: {
info: { title: 'API', version: '1.0.0' },
components: {
securitySchemes: {
bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
},
},
},
});
await fastify.register(swaggerUi, {
routePrefix: '/docs',
});
});
plugins/sensible.tsimport fp from 'fastify-plugin';
import sensible from '@fastify/sensible';
export default fp(async (fastify) => {
await fastify.register(sensible);
// Provides: fastify.httpErrors.notFound(), .badRequest(), etc.
});
routes/users/schema.tsimport { Type, Static } from '@sinclair/typebox';
export const UserSchema = Type.Object({
id: Type.String({ format: 'uuid' }),
email: Type.String({ format: 'email' }),
name: Type.String(),
createdAt: Type.String({ format: 'date-time' }),
});
export const CreateUserSchema = Type.Object({
email: Type.String({ format: 'email' }),
password: Type.String({ minLength: 8 }),
name: Type.String({ minLength: 1 }),
});
export const UserParamsSchema = Type.Object({
id: Type.String({ format: 'uuid' }),
});
export type User = Static<typeof UserSchema>;
export type CreateUserInput = Static<typeof CreateUserSchema>;
routes/users/index.tsimport { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox';
import { CreateUserSchema, UserSchema, UserParamsSchema } from './schema.js';
import * as usersService from '../../services/users.service.js';
const usersRoutes: FastifyPluginAsyncTypebox = async (fastify) => {
// POST /api/users
fastify.post('/', {
schema: {
tags: ['users'],
body: CreateUserSchema,
response: { 201: UserSchema },
},
}, async (request, reply) => {
const user = await usersService.create(request.body);
return reply.status(201).send(user);
});
// GET /api/users/:id
fastify.get('/:id', {
schema: {
tags: ['users'],
params: UserParamsSchema,
response: { 200: UserSchema },
},
}, async (request, reply) => {
const user = await usersService.findById(request.params.id);
if (!user) throw fastify.httpErrors.notFound(`User ${request.params.id} not found`);
return reply.send(user);
});
};
export default usersRoutes;
services/users.service.tsimport { CreateUserInput, User } from '../routes/users/schema.js';
// Replace with real database client
const users = new Map<string, User>();
export async function create(input: CreateUserInput): Promise<User> {
const user: User = {
id: crypto.randomUUID(),
email: input.email,
name: input.name,
createdAt: new Date().toISOString(),
};
users.set(user.id, user);
return user;
}
export async function findById(id: string): Promise<User | undefined> {
return users.get(id);
}
plugins/auth.tsimport fp from 'fastify-plugin';
import { FastifyRequest } from 'fastify';
export default fp(async (fastify) => {
// Decorate request with user property
fastify.decorateRequest('user', null);
// Reusable preHandler hook for protected routes
fastify.decorate('authenticate', async (request: FastifyRequest) => {
const authHeader = request.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
throw fastify.httpErrors.unauthorized('Missing bearer token');
}
const token = authHeader.slice(7);
try {
// Replace with real JWT verification
const payload = JSON.parse(atob(token.split('.')[1]));
request.user = payload;
} catch {
throw fastify.httpErrors.unauthorized('Invalid token');
}
});
});
// Usage in a route:
// fastify.get('/me', { preHandler: [fastify.authenticate] }, async (request) => {
// return request.user;
// });
// In app.ts or as a plugin
app.setErrorHandler((error, request, reply) => {
request.log.error(error);
// Fastify validation errors
if (error.validation) {
return reply.status(400).send({
error: 'Validation Error',
message: error.message,
details: error.validation,
});
}
const statusCode = error.statusCode ?? 500;
reply.status(statusCode).send({
error: statusCode >= 500 ? 'Internal Server Error' : error.message,
statusCode,
});
});
routes/health/index.tsimport { FastifyPluginAsync } from 'fastify';
const healthRoutes: FastifyPluginAsync = async (fastify) => {
fastify.get('/', {
schema: {
tags: ['health'],
response: {
200: {
type: 'object',
properties: {
status: { type: 'string' },
uptime: { type: 'number' },
},
},
},
},
}, async () => {
return { status: 'ok', uptime: process.uptime() };
});
};
export default healthRoutes;
.env.example to .env and fill in valuespnpm installpnpm tsx watch src/server.tscurl http://localhost:3000/api/health# Development with hot reload
pnpm tsx watch src/server.ts
# Build
pnpm tsup src/server.ts --format esm --dts
# Production
node dist/server.js
# Type check
pnpm tsc --noEmit
# Test (vitest recommended)
pnpm vitest run
# Print registered routes
# (programmatic: call app.printRoutes() after app.ready())
# View generated Swagger
# Open http://localhost:3000/docs after starting the server
fastify.db with the client instance.@fastify/jwt for built-in JWT sign/verify support. Decorate requests with the verified payload.@fastify/websocket for native WebSocket support within the plugin system.@fastify/rate-limit — configurable per-route or global.@fastify/multipart for streaming or buffered file uploads.app.inject() for in-process HTTP testing without starting a real server. This is Fastify's killer testing feature.response schemas. Fastify uses fast-json-stringify to serialize 2-3x faster than JSON.stringify and also strips undeclared properties (security benefit).testing
Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
testing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
testing
Set up Playwright 1.49+ with TypeScript for E2E testing using page object model, fixtures, test.describe/test blocks, assertions, selectors, network mocking, CI configuration, and trace viewer.
testing
Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.