install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/TerminalSkills/skills/fastify" ~/.claude/skills/comeonoliver-skillshub-fastify && rm -rf "$T"
manifest:
skills/TerminalSkills/skills/fastify/SKILL.mdsource content
Fastify
Fastify is one of the fastest Node.js web frameworks. It validates requests via JSON Schema, serializes responses automatically, and organizes code through an encapsulated plugin system.
Installation
# Create Fastify project mkdir my-api && cd my-api npm init -y npm i fastify @fastify/autoload @fastify/sensible @fastify/cors @fastify/jwt
Project Structure
# Recommended Fastify project layout src/ ├── app.js # App factory ├── server.js # Entry point ├── plugins/ # Shared plugins (db, auth) │ ├── db.js │ └── auth.js ├── routes/ # Route modules │ ├── articles/ │ │ ├── index.js # Route handler │ │ └── schema.js # JSON schemas │ └── health.js └── test/ └── articles.test.js
App Setup
// src/app.js — application factory with autoload import Fastify from 'fastify'; import autoload from '@fastify/autoload'; import sensible from '@fastify/sensible'; import cors from '@fastify/cors'; import { join } from 'node:path'; export function buildApp(opts = {}) { const app = Fastify({ logger: true, ...opts }); app.register(sensible); app.register(cors, { origin: true }); app.register(autoload, { dir: join(import.meta.dirname, 'plugins') }); app.register(autoload, { dir: join(import.meta.dirname, 'routes'), options: { prefix: '/api' } }); return app; }
// src/server.js — start the server import { buildApp } from './app.js'; const app = buildApp(); app.listen({ port: 3000, host: '0.0.0.0' }, (err) => { if (err) { app.log.error(err); process.exit(1); } });
Routes with Schema Validation
// src/routes/articles/schema.js — JSON Schema definitions export const articleSchema = { type: 'object', properties: { id: { type: 'integer' }, title: { type: 'string' }, body: { type: 'string' }, createdAt: { type: 'string', format: 'date-time' }, }, }; export const createArticleSchema = { body: { type: 'object', required: ['title', 'body'], properties: { title: { type: 'string', maxLength: 200 }, body: { type: 'string' }, }, }, response: { 201: articleSchema }, };
// src/routes/articles/index.js — article CRUD routes import { createArticleSchema } from './schema.js'; export default async function articleRoutes(fastify) { fastify.get('/', async (request) => { const { page = 1, limit = 20 } = request.query; const offset = (page - 1) * limit; const { rows } = await fastify.db.query( 'SELECT * FROM articles ORDER BY created_at DESC LIMIT $1 OFFSET $2', [limit, offset] ); return rows; }); fastify.get('/:id', async (request, reply) => { const { rows } = await fastify.db.query('SELECT * FROM articles WHERE id = $1', [request.params.id]); if (!rows[0]) return reply.notFound(); return rows[0]; }); fastify.post('/', { schema: createArticleSchema, preHandler: [fastify.authenticate] }, async (request, reply) => { const { title, body } = request.body; const { rows } = await fastify.db.query( 'INSERT INTO articles (title, body) VALUES ($1, $2) RETURNING *', [title, body] ); return reply.code(201).send(rows[0]); }); }
Plugins
// src/plugins/db.js — database plugin with pg import fp from 'fastify-plugin'; import pg from 'pg'; export default fp(async function dbPlugin(fastify) { const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL }); fastify.decorate('db', pool); fastify.addHook('onClose', () => pool.end()); });
// src/plugins/auth.js — JWT auth plugin import fp from 'fastify-plugin'; import jwt from '@fastify/jwt'; export default fp(async function authPlugin(fastify) { fastify.register(jwt, { secret: process.env.JWT_SECRET || 'dev-secret' }); fastify.decorate('authenticate', async function (request, reply) { try { await request.jwtVerify(); } catch (err) { reply.unauthorized(); } }); });
Hooks
// src/app.js — lifecycle hooks example (add inside buildApp) app.addHook('onRequest', async (request) => { request.startTime = process.hrtime.bigint(); }); app.addHook('onResponse', async (request, reply) => { const duration = Number(process.hrtime.bigint() - request.startTime) / 1e6; request.log.info({ duration: `${duration.toFixed(2)}ms`, status: reply.statusCode }, 'request completed'); });
Error Handling
// src/app.js — custom error handler (add inside buildApp) app.setErrorHandler((error, request, reply) => { request.log.error(error); const status = error.statusCode || 500; reply.code(status).send({ error: error.name, message: status === 500 ? 'Internal Server Error' : error.message, }); });
Testing
// src/test/articles.test.js — testing with built-in inject import { test } from 'node:test'; import assert from 'node:assert'; import { buildApp } from '../app.js'; test('GET /api/articles returns 200', async () => { const app = buildApp({ logger: false }); const response = await app.inject({ method: 'GET', url: '/api/articles' }); assert.strictEqual(response.statusCode, 200); await app.close(); });
Key Patterns
- Use
(fastify-plugin
) for plugins that should share the same encapsulation contextfp - Use JSON Schema for validation — it also generates automatic serialization for speed
- Use
to auto-register plugins and routes from directories@fastify/autoload - Decorate the fastify instance (
) for shared services (db, cache)fastify.decorate - Use
for@fastify/sensible
,.notFound()
, etc. helpers.unauthorized() - Fastify is async-first: return values from handlers instead of calling
reply.send()