atelier-typescript-fastify

Fast, low-overhead web framework for Node.js with TypeBox schema validation.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "atelier-typescript-fastify" with this command: npx skills add martinffx/claude-code-atelier/martinffx-claude-code-atelier-atelier-typescript-fastify

Fastify

Fast, low-overhead web framework for Node.js with TypeBox schema validation.

Additional References

  • references/plugins.md - Plugin architecture and dependency injection

  • references/typeid.md - Type-safe prefixed identifiers

Setup

npm i fastify @fastify/type-provider-typebox @sinclair/typebox

import Fastify from 'fastify' import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'

const app = Fastify({ logger: true }).withTypeProvider<TypeBoxTypeProvider>()

Schema Definition

import { Type, Static } from '@sinclair/typebox'

// Request/response schemas with $id for OpenAPI export const UserSchema = Type.Object({ id: Type.String({ format: 'uuid' }), name: Type.String({ minLength: 1, maxLength: 100 }), email: Type.String({ format: 'email' }), createdAt: Type.String({ format: 'date-time' }), }, { $id: 'UserResponse' })

export type User = Static<typeof UserSchema>

// Input schemas (omit generated fields) export const CreateUserSchema = Type.Object({ name: Type.String({ minLength: 1, maxLength: 100 }), email: Type.String({ format: 'email' }), }, { $id: 'CreateUserRequest' })

export type CreateUserInput = Static<typeof CreateUserSchema>

Route with Full Schema

const TAGS = ['Users']

app.post('/users', { schema: { operationId: 'createUser', tags: TAGS, summary: 'Create a new user', description: 'Create a new user account', body: CreateUserSchema, response: { 201: UserSchema, 400: BadRequestErrorResponse, 401: UnauthorizedErrorResponse, 500: InternalServerErrorResponse, }, }, }, async (request, reply) => { const { name, email } = request.body // fully typed

const user = await createUser({ name, email }) return reply.status(201).send(user) })

Common Schema Patterns

// Path parameters const ParamsSchema = Type.Object({ id: Type.String({ format: 'uuid' }), })

// Query string with pagination const QuerySchema = Type.Object({ limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100, default: 20 })), cursor: Type.Optional(Type.String()), sort: Type.Optional(Type.Union([Type.Literal('asc'), Type.Literal('desc')])), })

// Paginated response wrapper const PaginatedResponse = <T extends TSchema>(itemSchema: T) => Type.Object({ items: Type.Array(itemSchema), nextCursor: Type.Optional(Type.String()), hasMore: Type.Boolean(), })

app.get('/users/:id', { schema: { operationId: 'getUser', tags: ['Users'], summary: 'Get user by ID', params: ParamsSchema, querystring: QuerySchema, response: { 200: UserSchema, 400: BadRequestErrorResponse, 404: NotFoundErrorResponse, 500: InternalServerErrorResponse, }, }, }, async (request, reply) => { const { id } = request.params const { limit, cursor } = request.query // ... })

Modular Route Registration

// types.ts - Export typed Fastify instance import { FastifyInstance, FastifyBaseLogger, RawReplyDefaultExpression, RawRequestDefaultExpression, RawServerDefault, } from 'fastify' import { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'

export type FastifyTypebox = FastifyInstance< RawServerDefault, RawRequestDefaultExpression<RawServerDefault>, RawReplyDefaultExpression<RawServerDefault>, FastifyBaseLogger, TypeBoxTypeProvider

// routes/users.ts import { Type } from '@sinclair/typebox' import { FastifyTypebox } from '../types'

export async function userRoutes(app: FastifyTypebox) { app.get('/users', { schema: { response: { 200: Type.Array(UserSchema), }, }, }, async () => { return await listUsers() }) }

// index.ts import { userRoutes } from './routes/users'

app.register(userRoutes, { prefix: '/api/v1' })

Error Schemas (RFC 7807)

Use standardized error responses across all routes:

import { Type } from '@sinclair/typebox'

// Base ProblemDetail schema (RFC 7807) const ProblemDetail = Type.Object({ type: Type.String(), status: Type.Number(), title: Type.String(), detail: Type.String(), instance: Type.String(), traceId: Type.String(), })

// Specific error responses export const BadRequestErrorResponse = Type.Composite([ ProblemDetail, Type.Object({ type: Type.Literal('BAD_REQUEST'), status: Type.Literal(400), }), ], { $id: 'BadRequestErrorResponse' })

export const UnauthorizedErrorResponse = Type.Composite([ ProblemDetail, Type.Object({ type: Type.Literal('UNAUTHORIZED'), status: Type.Literal(401), }), ], { $id: 'UnauthorizedErrorResponse' })

export const ForbiddenErrorResponse = Type.Composite([ ProblemDetail, Type.Object({ type: Type.Literal('FORBIDDEN'), status: Type.Literal(403), }), ], { $id: 'ForbiddenErrorResponse' })

export const NotFoundErrorResponse = Type.Composite([ ProblemDetail, Type.Object({ type: Type.Literal('NOT_FOUND'), status: Type.Literal(404), }), ], { $id: 'NotFoundErrorResponse' })

export const InternalServerErrorResponse = Type.Composite([ ProblemDetail, Type.Object({ type: Type.Literal('INTERNAL_SERVER_ERROR'), status: Type.Literal(500), }), ], { $id: 'InternalServerErrorResponse' })

Error Handling

import { FastifyError, FastifyRequest, FastifyReply } from 'fastify'

// Custom error handler const globalErrorHandler = ( error: FastifyError, request: FastifyRequest, reply: FastifyReply ) => { // Handle Fastify validation errors if (error.code === 'FST_ERR_VALIDATION') { return reply.status(400).send({ type: 'BAD_REQUEST', status: 400, title: 'Validation Error', detail: error.message, instance: request.url, traceId: request.id, }) }

// Handle domain errors (if using error classes) if (error instanceof AppError) { return reply.status(error.status).send(error.toResponse()) }

// Default to internal server error request.log.error(error) return reply.status(500).send({ type: 'INTERNAL_SERVER_ERROR', status: 500, title: 'Internal Server Error', detail: 'Something went wrong', instance: request.url, traceId: request.id, }) }

app.setErrorHandler(globalErrorHandler)

Reusable Schemas (Shared References)

// Add schema to instance for $ref usage app.addSchema({ $id: 'User', ...UserSchema, })

app.addSchema({ $id: 'Error', ...ErrorSchema, })

// Reference in routes app.get('/me', { schema: { response: { 200: Type.Ref('User'), 401: Type.Ref('Error'), }, }, }, handler)

Headers and Auth

const AuthHeadersSchema = Type.Object({ authorization: Type.String({ pattern: '^Bearer .+$' }), })

app.get('/protected', { schema: { headers: AuthHeadersSchema, response: { 200: UserSchema, 401: UnauthorizedErrorResponse, }, }, preValidation: async (request, reply) => { const token = request.headers.authorization?.replace('Bearer ', '') if (!token || !verifyToken(token)) { throw new UnauthorizedError('Invalid or missing token') } }, }, handler)

Auth & Permissions

Role-based permission checks with decorators:

import type { FastifyRequest, FastifyReply } from 'fastify'

// Permission constants const Permissions = [ 'user:read', 'user:write', 'user:delete', 'admin:access', ] as const

type Permission = typeof Permissions[number]

// Role-based permission sets const RolePermissions = { admin: new Set<Permission>(['user:read', 'user:write', 'user:delete', 'admin:access']), user: new Set<Permission>(['user:read', 'user:write']), readonly: new Set<Permission>(['user:read']), } as const

// Extend FastifyRequest with token data declare module 'fastify' { interface FastifyRequest { token: { userId: string role: keyof typeof RolePermissions permissions: Permission[] } } }

// Permission check decorator app.decorate('hasPermissions', (requiredPermissions: Permission[]) => { return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => { const userPermissions = request.token.permissions

for (const permission of requiredPermissions) {
  if (!userPermissions.includes(permission)) {
    throw new ForbiddenError(`Missing permission: ${permission}`)
  }
}

} })

// Usage in routes app.delete('/users/:id', { schema: { operationId: 'deleteUser', tags: ['Users'], params: Type.Object({ id: Type.String() }), response: { 204: Type.Null(), 401: UnauthorizedErrorResponse, 403: ForbiddenErrorResponse, 404: NotFoundErrorResponse, }, }, preHandler: app.hasPermissions(['user:delete']), }, async (request, reply) => { await deleteUser(request.params.id) return reply.status(204).send() })

Guidelines

  • Always define schemas with Type.Object({ ... })

  • full JSON Schema required in Fastify v5

  • Add $id to all schemas for OpenAPI generation and reusability

  • Add operationId , tags , and summary to all routes for documentation

  • Define response schemas for ALL status codes (200, 400, 401, 403, 404, 500)

  • Use RFC 7807 ProblemDetail format for errors with Type.Composite

  • Use Static<typeof Schema> to derive TypeScript types from schemas

  • Split input schemas (CreateX) from output schemas (X) - omit generated fields

  • Use Type.Optional() for optional fields, not ? in the type

  • Export FastifyTypebox type for modular route files

  • Add format validators: uuid , email , date-time , uri

  • Use Type.Union([Type.Literal(...)]) for string enums

  • Use Fastify plugins with fp() for dependency injection - see references/plugins.md

  • Use preHandler with hasPermissions() decorator for protected routes

  • Use TypeID for type-safe prefixed identifiers - see references/typeid.md

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

python:architecture

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

python:build-tools

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

python:sqlalchemy

No summary provided by upstream source.

Repository SourceNeeds Review