AgentStack
SKILL verified MIT Self-run

Typescript Fastify

skill-martinffx-atelier-typescript-fastify · by martinffx

Building REST APIs with Fastify in TypeScript. Use when creating routes, handling requests, implementing validation with TypeBox, structuring applications, or working with HTTP handlers and plugins.

No reviews yet
0 installs
15 views
0.0% view→install

Install

$ agentstack add skill-martinffx-atelier-typescript-fastify

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Typescript Fastify? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Fastify

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

Additional References

  • [references/plugins.md](./references/plugins.md) - Plugin architecture and dependency injection
  • [references/typeid.md](./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()

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

// 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

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 = (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,
  RawReplyDefaultExpression,
  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(['user:read', 'user:write', 'user:delete', 'admin:access']),
  user: new Set(['user:read', 'user:write']),
  readonly: new Set(['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 => {
    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

  1. Always define schemas with Type.Object({ ... }) - full JSON Schema required in Fastify v5
  2. Add $id to all schemas for OpenAPI generation and reusability
  3. Add operationId, tags, and summary to all routes for documentation
  4. Define response schemas for ALL status codes (200, 400, 401, 403, 404, 500)
  5. Use RFC 7807 ProblemDetail format for errors with Type.Composite
  6. Use Static to derive TypeScript types from schemas
  7. Split input schemas (CreateX) from output schemas (X) - omit generated fields
  8. Use Type.Optional() for optional fields, not ? in the type
  9. Export FastifyTypebox type for modular route files
  10. Add format validators: uuid, email, date-time, uri
  11. Use Type.Union([Type.Literal(...)]) for string enums
  12. Use Fastify plugins with fp() for dependency injection - see [references/plugins.md](./references/plugins.md)
  13. Use preHandler with hasPermissions() decorator for protected routes
  14. Use TypeID for type-safe prefixed identifiers - see [references/typeid.md](./references/typeid.md)

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.