home / skills / blockmatic-icebox / basilic-old / fastify-v5

This skill enables building type-safe Fastify REST APIs using TypeBox schemas, automatic OpenAPI generation, and robust request/response validation.

npx playbooks add skill blockmatic-icebox/basilic-old --skill fastify-v5

Review the files below or copy the command above to add this skill to your agents.

Files (10)
SKILL.md
6.8 KB
---
name: TypeBox + Fastify
description: |
  TypeBox schemas with Fastify routes for type-safe API development. Native JSON Schema, automatic OpenAPI generation.
  
  Use when: building Fastify REST APIs with schema validation and OpenAPI generation.
---

# Skill: fastify

## Scope

- Applies to: Fastify v5+ with TypeBox schemas, type-safe route definitions, automatic OpenAPI generation, plugins, hooks, testing
- Does NOT cover: Database integration (see [drizzle-orm](@cursor/skills/drizzle-orm-v0/SKILL.md))

## Assumptions

- Fastify v5+
- `@fastify/type-provider-typebox` for TypeBox type provider
- `@sinclair/typebox` for schema definitions
- TypeScript v5+ with strict mode

## Principles

- Use TypeBox schemas for route validation (params, query, body, response)
- Use `TypeBoxTypeProvider` for automatic type inference from schemas
- Define response schemas for all status codes
- Use OpenAPI generation from schemas (via `@fastify/swagger`)
- Request types (`request.params`, `request.query`, `request.body`) are inferred from schemas
- Response validation happens automatically based on schema

## Constraints

### MUST

- Use `TypeBoxTypeProvider` for type safety: `fastify.withTypeProvider<TypeBoxTypeProvider>()`
- Define response schemas for all status codes
- Use TypeBox `Type.*` constructors for schema definitions

### SHOULD

- Use `operationId` in route schemas for OpenAPI generation
- Use `tags` for route organization in OpenAPI docs
- Define error response schemas for error cases

### AVOID

- Manual type assertions (types inferred from schemas)
- Skipping response schema definitions
- Mixing TypeBox with other schema libraries in same route

## Interactions

- Complements [drizzle-orm](@cursor/skills/drizzle-orm-v0/SKILL.md) for database integration
- Generates OpenAPI specs compatible with [openapi-ts](@cursor/skills/openapi-ts-v0/SKILL.md) codegen

## Patterns

### Route Schema Pattern

```typescript
import { Type } from '@sinclair/typebox'
import type { FastifyPluginAsync } from 'fastify'

const UserSchema = Type.Object({
  id: Type.String(),
  email: Type.String({ format: 'email' }),
  name: Type.String(),
})

const userRoutes: FastifyPluginAsync = async fastify => {
  fastify.get('/users/:id', {
    schema: {
      operationId: 'getUser',
      params: Type.Object({ id: Type.String() }),
      response: {
        200: UserSchema,
        404: Type.Object({ code: Type.String(), message: Type.String() }),
      },
    },
  }, async (request, reply) => {
    // request.params.id is typed
    const { id } = request.params
    return reply.send(user)
  })
}
```

### Schema Types

```typescript
Type.String()
Type.Number()
Type.Boolean()
Type.Object({ id: Type.String() })
Type.Array(Type.String())
Type.Optional(Type.String())
Type.Union([Type.String(), Type.Number()])
```

See [Route Schema Template](templates/route-schema.ts) for complete example.

### Instance Configuration

Configure Fastify instance with TypeBox type provider, logger, and request settings:

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

const fastify = Fastify({
  logger: {
    level: 'info',
    transport: process.env.NODE_ENV === 'development' ? {
      target: 'pino-pretty',
    } : undefined,
  },
  trustProxy: true,
  bodyLimit: 1048576, // 1MB
  requestTimeout: 30000,
  requestIdHeader: 'x-request-id',
  requestIdLogLabel: 'reqId',
}).withTypeProvider<TypeBoxTypeProvider>()
```

See [Instance Configuration](references/instance-config.md) for complete setup.

### Plugin Development

Create reusable plugins with `fastify-plugin` for non-encapsulated behavior:

```typescript
import type { FastifyPluginAsync } from 'fastify'
import fp from 'fastify-plugin'

const myPlugin: FastifyPluginAsync = async fastify => {
  // Plugin logic
}

export default fp(myPlugin, {
  name: 'my-plugin',
})
```

See [Plugin Patterns](references/plugins.md) and [Plugin Template](templates/plugin.ts) for examples.

### Hooks (Request Lifecycle)

Use hooks to intercept requests, modify responses, or handle errors:

```typescript
fastify.addHook('onRequest', async (request, reply) => {
  // Add security headers, logging, etc.
})

fastify.addHook('onResponse', async (request, reply) => {
  // Modify response, add headers, etc.
})

fastify.addHook('onError', async (request, reply, error) => {
  // Handle errors
})
```

See [Hooks Guide](references/hooks.md) and [Hook Plugin Template](templates/hook-plugin.ts) for patterns.

### Testing with inject()

Test routes using Fastify's `inject()` method for blackbox testing:

```typescript
const response = await fastify.inject({
  method: 'GET',
  url: '/users/123',
})

expect(response.statusCode).toBe(200)
const data = JSON.parse(response.body)
```

See [Testing Patterns](references/testing.md) and [Test Utils Template](templates/test-utils.ts) for setup.

### Streaming Responses

Handle streaming responses (e.g., Server-Sent Events, text streams):

```typescript
fastify.post('/stream', {
  schema: {
    response: {
      200: Type.String({ description: 'Streaming response' }),
    },
  },
}, async (request, reply) => {
  reply.header('Content-Type', 'text/event-stream')
  reply.header('Cache-Control', 'no-cache')
  return reply.send(stream)
})
```

### Common Plugins

#### Rate Limiting

```typescript
import rateLimit from '@fastify/rate-limit'

await fastify.register(rateLimit, {
  max: 100,
  timeWindow: 60000,
  keyGenerator: request => request.ip,
})
```

#### CORS

```typescript
import cors from '@fastify/cors'

await fastify.register(cors, {
  origin: (origin, callback) => {
    // Validate origin
    callback(null, true)
  },
  credentials: false,
})
```

#### Error Handler

```typescript
fastify.setErrorHandler((error, request, reply) => {
  // Global error handling
  reply.status(error.statusCode ?? 500).send({
    code: 'ERROR',
    message: error.message,
  })
})
```

### Graceful Shutdown

Handle process signals for graceful shutdown:

```typescript
const shutdown = async (signal: string) => {
  fastify.log.info({ signal }, 'Shutting down')
  await fastify.close()
  process.exit(0)
}

process.on('SIGTERM', () => shutdown('SIGTERM'))
process.on('SIGINT', () => shutdown('SIGINT'))
```

### AutoLoad Pattern

Use `@fastify/autoload` for automatic plugin/route discovery:

```typescript
import AutoLoad from '@fastify/autoload'

await fastify.register(AutoLoad, {
  dir: path.join(__dirname, 'plugins'),
  forceESM: true,
})
```

## References

- [OpenAPI Integration](references/openapi-integration.md) - OpenAPI generation patterns
- [Plugin Patterns](references/plugins.md) - Plugin development with fastify-plugin
- [Hooks Guide](references/hooks.md) - Request lifecycle hooks
- [Testing Patterns](references/testing.md) - Testing with inject()
- [Instance Configuration](references/instance-config.md) - Fastify instance setup

Overview

This skill integrates TypeBox schemas with Fastify to build type-safe REST APIs in TypeScript. It provides patterns for schema-driven route definitions, automatic JSON Schema-based validation, and OpenAPI generation. Use it to enforce request/response contracts and get end-to-end type inference from schemas.

How this skill works

Create TypeBox schemas for params, query, body, and responses and attach them to Fastify route schema objects. Instantiate Fastify with the TypeBox type provider so request types are inferred from schemas and response validation runs automatically. Configure OpenAPI generation via Fastify Swagger plugins using operationId and tags for clean documentation.

When to use it

  • Building Fastify v5+ REST APIs with full TypeScript type safety
  • Enforcing runtime validation for request params, query, body, and responses
  • Generating OpenAPI/Swagger docs directly from route schemas
  • Developing reusable plugins or hooks that rely on typed request shapes
  • Writing blackbox tests using Fastify inject() with known input/output contracts

Best practices

  • Always use fastify.withTypeProvider<TypeBoxTypeProvider>() for routes
  • Define response schemas for every expected status code, including errors
  • Prefer Type.* constructors (Type.Object, Type.String, etc.) for schemas
  • Include operationId and tags in schema for consistent OpenAPI output
  • Avoid mixing different schema libraries in the same route to prevent conflicts

Example use cases

  • User CRUD API: params, body and 200/404 response schemas for typed handlers
  • Auth endpoints: validate request bodies and define standardized error responses
  • File streaming or SSE endpoints: declare response type and set proper headers
  • Plugin development: build fastify-plugin modules that register typed routes
  • Automated API docs: emit OpenAPI spec from TypeBox schemas for client codegen

FAQ

Do I still need manual TypeScript types for requests and responses?

No — when you use TypeBox schemas with TypeBoxTypeProvider, request.params, request.query and request.body are inferred from the schemas; define response schemas to get validation and typing for outputs.

How do I include error formats in OpenAPI?

Define error response schemas for each status code in the route schema and include descriptive examples, operationId, and tags. The swagger plugin will incorporate those into the generated spec.