{"id":"51d0f272-81a0-4ae3-9956-1299e395e56d","shortId":"7NZfCJ","kind":"skill","title":"typescript-fastify","tagline":"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.","description":"# Fastify\n\nFast, low-overhead web framework for Node.js with TypeBox schema validation.\n\n## Additional References\n\n- [references/plugins.md](./references/plugins.md) - Plugin architecture and dependency injection\n- [references/typeid.md](./references/typeid.md) - Type-safe prefixed identifiers\n\n## Setup\n\n```bash\nnpm i fastify @fastify/type-provider-typebox @sinclair/typebox\n```\n\n```typescript\nimport Fastify from 'fastify'\nimport { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'\n\nconst app = Fastify({ logger: true }).withTypeProvider<TypeBoxTypeProvider>()\n```\n\n## Schema Definition\n\n```typescript\nimport { Type, Static } from '@sinclair/typebox'\n\n// Request/response schemas with $id for OpenAPI\nexport const UserSchema = Type.Object({\n  id: Type.String({ format: 'uuid' }),\n  name: Type.String({ minLength: 1, maxLength: 100 }),\n  email: Type.String({ format: 'email' }),\n  createdAt: Type.String({ format: 'date-time' }),\n}, { $id: 'UserResponse' })\n\nexport type User = Static<typeof UserSchema>\n\n// Input schemas (omit generated fields)\nexport const CreateUserSchema = Type.Object({\n  name: Type.String({ minLength: 1, maxLength: 100 }),\n  email: Type.String({ format: 'email' }),\n}, { $id: 'CreateUserRequest' })\n\nexport type CreateUserInput = Static<typeof CreateUserSchema>\n```\n\n## Route with Full Schema\n\n```typescript\nconst TAGS = ['Users']\n\napp.post('/users', {\n  schema: {\n    operationId: 'createUser',\n    tags: TAGS,\n    summary: 'Create a new user',\n    description: 'Create a new user account',\n    body: CreateUserSchema,\n    response: {\n      201: UserSchema,\n      400: BadRequestErrorResponse,\n      401: UnauthorizedErrorResponse,\n      500: InternalServerErrorResponse,\n    },\n  },\n}, async (request, reply) => {\n  const { name, email } = request.body // fully typed\n\n  const user = await createUser({ name, email })\n  return reply.status(201).send(user)\n})\n```\n\n## Common Schema Patterns\n\n```typescript\n// Path parameters\nconst ParamsSchema = Type.Object({\n  id: Type.String({ format: 'uuid' }),\n})\n\n// Query string with pagination\nconst QuerySchema = Type.Object({\n  limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100, default: 20 })),\n  cursor: Type.Optional(Type.String()),\n  sort: Type.Optional(Type.Union([Type.Literal('asc'), Type.Literal('desc')])),\n})\n\n// Paginated response wrapper\nconst PaginatedResponse = <T extends TSchema>(itemSchema: T) =>\n  Type.Object({\n    items: Type.Array(itemSchema),\n    nextCursor: Type.Optional(Type.String()),\n    hasMore: Type.Boolean(),\n  })\n\napp.get('/users/:id', {\n  schema: {\n    operationId: 'getUser',\n    tags: ['Users'],\n    summary: 'Get user by ID',\n    params: ParamsSchema,\n    querystring: QuerySchema,\n    response: {\n      200: UserSchema,\n      400: BadRequestErrorResponse,\n      404: NotFoundErrorResponse,\n      500: InternalServerErrorResponse,\n    },\n  },\n}, async (request, reply) => {\n  const { id } = request.params\n  const { limit, cursor } = request.query\n  // ...\n})\n```\n\n## Modular Route Registration\n\n```typescript\n// types.ts - Export typed Fastify instance\nimport {\n  FastifyInstance,\n  FastifyBaseLogger,\n  RawReplyDefaultExpression,\n  RawRequestDefaultExpression,\n  RawServerDefault,\n} from 'fastify'\nimport { TypeBoxTypeProvider } from '@fastify/type-provider-typebox'\n\nexport type FastifyTypebox = FastifyInstance<\n  RawServerDefault,\n  RawRequestDefaultExpression<RawServerDefault>,\n  RawReplyDefaultExpression<RawServerDefault>,\n  FastifyBaseLogger,\n  TypeBoxTypeProvider\n>\n```\n\n```typescript\n// routes/users.ts\nimport { Type } from '@sinclair/typebox'\nimport { FastifyTypebox } from '../types'\n\nexport async function userRoutes(app: FastifyTypebox) {\n  app.get('/users', {\n    schema: {\n      response: {\n        200: Type.Array(UserSchema),\n      },\n    },\n  }, async () => {\n    return await listUsers()\n  })\n}\n```\n\n```typescript\n// index.ts\nimport { userRoutes } from './routes/users'\n\napp.register(userRoutes, { prefix: '/api/v1' })\n```\n\n## Error Schemas (RFC 7807)\n\nUse standardized error responses across all routes:\n\n```typescript\nimport { Type } from '@sinclair/typebox'\n\n// Base ProblemDetail schema (RFC 7807)\nconst ProblemDetail = Type.Object({\n  type: Type.String(),\n  status: Type.Number(),\n  title: Type.String(),\n  detail: Type.String(),\n  instance: Type.String(),\n  traceId: Type.String(),\n})\n\n// Specific error responses\nexport const BadRequestErrorResponse = Type.Composite([\n  ProblemDetail,\n  Type.Object({\n    type: Type.Literal('BAD_REQUEST'),\n    status: Type.Literal(400),\n  }),\n], { $id: 'BadRequestErrorResponse' })\n\nexport const UnauthorizedErrorResponse = Type.Composite([\n  ProblemDetail,\n  Type.Object({\n    type: Type.Literal('UNAUTHORIZED'),\n    status: Type.Literal(401),\n  }),\n], { $id: 'UnauthorizedErrorResponse' })\n\nexport const ForbiddenErrorResponse = Type.Composite([\n  ProblemDetail,\n  Type.Object({\n    type: Type.Literal('FORBIDDEN'),\n    status: Type.Literal(403),\n  }),\n], { $id: 'ForbiddenErrorResponse' })\n\nexport const NotFoundErrorResponse = Type.Composite([\n  ProblemDetail,\n  Type.Object({\n    type: Type.Literal('NOT_FOUND'),\n    status: Type.Literal(404),\n  }),\n], { $id: 'NotFoundErrorResponse' })\n\nexport const InternalServerErrorResponse = Type.Composite([\n  ProblemDetail,\n  Type.Object({\n    type: Type.Literal('INTERNAL_SERVER_ERROR'),\n    status: Type.Literal(500),\n  }),\n], { $id: 'InternalServerErrorResponse' })\n```\n\n## Error Handling\n\n```typescript\nimport { FastifyError, FastifyRequest, FastifyReply } from 'fastify'\n\n// Custom error handler\nconst globalErrorHandler = (\n  error: FastifyError,\n  request: FastifyRequest,\n  reply: FastifyReply\n) => {\n  // Handle Fastify validation errors\n  if (error.code === 'FST_ERR_VALIDATION') {\n    return reply.status(400).send({\n      type: 'BAD_REQUEST',\n      status: 400,\n      title: 'Validation Error',\n      detail: error.message,\n      instance: request.url,\n      traceId: request.id,\n    })\n  }\n\n  // Handle domain errors (if using error classes)\n  if (error instanceof AppError) {\n    return reply.status(error.status).send(error.toResponse())\n  }\n\n  // Default to internal server error\n  request.log.error(error)\n  return reply.status(500).send({\n    type: 'INTERNAL_SERVER_ERROR',\n    status: 500,\n    title: 'Internal Server Error',\n    detail: 'Something went wrong',\n    instance: request.url,\n    traceId: request.id,\n  })\n}\n\napp.setErrorHandler(globalErrorHandler)\n```\n\n## Reusable Schemas (Shared References)\n\n```typescript\n// Add schema to instance for $ref usage\napp.addSchema({\n  $id: 'User',\n  ...UserSchema,\n})\n\napp.addSchema({\n  $id: 'Error',\n  ...ErrorSchema,\n})\n\n// Reference in routes\napp.get('/me', {\n  schema: {\n    response: {\n      200: Type.Ref('User'),\n      401: Type.Ref('Error'),\n    },\n  },\n}, handler)\n```\n\n## Headers and Auth\n\n```typescript\nconst AuthHeadersSchema = Type.Object({\n  authorization: Type.String({ pattern: '^Bearer .+$' }),\n})\n\napp.get('/protected', {\n  schema: {\n    headers: AuthHeadersSchema,\n    response: {\n      200: UserSchema,\n      401: UnauthorizedErrorResponse,\n    },\n  },\n  preValidation: async (request, reply) => {\n    const token = request.headers.authorization?.replace('Bearer ', '')\n    if (!token || !verifyToken(token)) {\n      throw new UnauthorizedError('Invalid or missing token')\n    }\n  },\n}, handler)\n```\n\n## Auth & Permissions\n\nRole-based permission checks with decorators:\n\n```typescript\nimport type { FastifyRequest, FastifyReply } from 'fastify'\n\n// Permission constants\nconst Permissions = [\n  'user:read',\n  'user:write',\n  'user:delete',\n  'admin:access',\n] as const\n\ntype Permission = typeof Permissions[number]\n\n// Role-based permission sets\nconst RolePermissions = {\n  admin: new Set<Permission>(['user:read', 'user:write', 'user:delete', 'admin:access']),\n  user: new Set<Permission>(['user:read', 'user:write']),\n  readonly: new Set<Permission>(['user:read']),\n} as const\n\n// Extend FastifyRequest with token data\ndeclare module 'fastify' {\n  interface FastifyRequest {\n    token: {\n      userId: string\n      role: keyof typeof RolePermissions\n      permissions: Permission[]\n    }\n  }\n}\n\n// Permission check decorator\napp.decorate('hasPermissions', (requiredPermissions: Permission[]) => {\n  return async (request: FastifyRequest, reply: FastifyReply): Promise<void> => {\n    const userPermissions = request.token.permissions\n\n    for (const permission of requiredPermissions) {\n      if (!userPermissions.includes(permission)) {\n        throw new ForbiddenError(`Missing permission: ${permission}`)\n      }\n    }\n  }\n})\n\n// Usage in routes\napp.delete('/users/:id', {\n  schema: {\n    operationId: 'deleteUser',\n    tags: ['Users'],\n    params: Type.Object({ id: Type.String() }),\n    response: {\n      204: Type.Null(),\n      401: UnauthorizedErrorResponse,\n      403: ForbiddenErrorResponse,\n      404: NotFoundErrorResponse,\n    },\n  },\n  preHandler: app.hasPermissions(['user:delete']),\n}, async (request, reply) => {\n  await deleteUser(request.params.id)\n  return reply.status(204).send()\n})\n```\n\n## Guidelines\n\n1. Always define schemas with `Type.Object({ ... })` - full JSON Schema required in Fastify v5\n2. Add `$id` to all schemas for OpenAPI generation and reusability\n3. Add `operationId`, `tags`, and `summary` to all routes for documentation\n4. Define response schemas for ALL status codes (200, 400, 401, 403, 404, 500)\n5. Use RFC 7807 ProblemDetail format for errors with `Type.Composite`\n6. Use `Static<typeof Schema>` to derive TypeScript types from schemas\n7. Split input schemas (CreateX) from output schemas (X) - omit generated fields\n8. Use `Type.Optional()` for optional fields, not `?` in the type\n9. Export `FastifyTypebox` type for modular route files\n10. Add format validators: `uuid`, `email`, `date-time`, `uri`\n11. Use `Type.Union([Type.Literal(...)])` for string enums\n12. Use Fastify plugins with `fp()` for dependency injection - see [references/plugins.md](./references/plugins.md)\n13. Use `preHandler` with `hasPermissions()` decorator for protected routes\n14. Use TypeID for type-safe prefixed identifiers - see [references/typeid.md](./references/typeid.md)","tags":["typescript","fastify","atelier","martinffx","agent-skills","agentic-coding","anthropic","claude-code","claude-skills","code-review","codex","codex-skill"],"capabilities":["skill","source-martinffx","skill-typescript-fastify","topic-agent-skills","topic-agentic-coding","topic-anthropic","topic-claude-code","topic-claude-skills","topic-code-review","topic-codex","topic-codex-skill","topic-opencode","topic-prompt-engineering","topic-sdd","topic-spec-driven-development"],"categories":["atelier"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/martinffx/atelier/typescript-fastify","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add martinffx/atelier","source_repo":"https://github.com/martinffx/atelier","install_from":"skills.sh"}},"qualityScore":"0.461","qualityRationale":"deterministic score 0.46 from registry signals: · indexed on github topic:agent-skills · 23 github stars · SKILL.md body (10,124 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-05-18T19:05:25.218Z","embedding":null,"createdAt":"2026-05-10T07:03:13.702Z","updatedAt":"2026-05-18T19:05:25.218Z","lastSeenAt":"2026-05-18T19:05:25.218Z","tsv":"'/api/v1':364 '/me':596 '/protected':618 '/references/plugins.md':46,930 '/references/typeid.md':53,951 '/routes/users':360 '/types':337 '/users':159,263,345,769 '1':106,137,231,804 '10':902 '100':108,139,233 '11':912 '12':919 '13':931 '14':940 '2':817 '20':235 '200':280,348,599,623,847 '201':179,204 '204':781,801 '3':828 '4':839 '400':181,282,416,509,515,848 '401':183,430,602,625,783,849 '403':444,785,850 '404':284,459,787,851 '5':853 '500':185,286,475,550,557,852 '6':863 '7':872 '7807':368,385,856 '8':884 '9':894 'access':675,700 'account':175 'across':373 'add':577,818,829,903 'addit':43 'admin':674,690,699 'alway':805 'api':6 'app':76,342 'app.addschema':584,588 'app.decorate':737 'app.delete':768 'app.get':262,344,595,617 'app.haspermissions':790 'app.post':158 'app.register':361 'app.seterrorhandler':570 'apperror':535 'applic':22 'architectur':48 'asc':243 'async':187,288,339,351,628,742,793 'auth':608,648 'authheadersschema':611,621 'author':613 'await':198,353,796 'bad':412,512 'badrequesterrorrespons':182,283,406,418 'base':381,652,685 'bash':60 'bearer':616,635 'bodi':176 'build':4 'check':654,735 'class':531 'code':846 'common':207 'const':75,96,131,155,190,196,213,224,249,291,294,386,405,420,434,448,463,490,610,631,666,677,688,714,748,752 'constant':665 'creat':13,166,171 'createdat':113 'createus':162,199 'createuserinput':148 'createuserrequest':145 'createuserschema':132,177 'createx':876 'cursor':236,296 'custom':487 'data':719 'date':117,909 'date-tim':116,908 'declar':720 'decor':656,736,936 'default':234,541 'defin':806,840 'definit':82 'delet':673,698,792 'deleteus':773,797 'depend':50,926 'deriv':867 'desc':245 'descript':170 'detail':395,519,562 'document':838 'domain':526 'email':109,112,140,143,192,201,907 'enum':918 'err':505 'error':365,371,402,472,478,488,492,501,518,527,530,533,545,547,555,561,590,604,860 'error.code':503 'error.message':520 'error.status':538 'error.toresponse':540 'errorschema':591 'export':95,121,130,146,303,319,338,404,419,433,447,462,895 'extend':715 'fast':31 'fastifi':3,8,30,63,68,70,77,305,314,486,499,663,722,815,921 'fastify/type-provider-typebox':64,74,318 'fastifybaselogg':309,326 'fastifyerror':482,493 'fastifyinst':308,322 'fastifyrepli':484,497,661,746 'fastifyrequest':483,495,660,716,724,744 'fastifytypebox':321,335,343,896 'field':129,883,889 'file':901 'forbidden':441 'forbiddenerror':761 'forbiddenerrorrespons':435,446,786 'format':101,111,115,142,218,858,904 'found':456 'fp':924 'framework':36 'fst':504 'full':152,810 'fulli':194 'function':340 'generat':128,825,882 'get':271 'getus':267 'globalerrorhandl':491,571 'guidelin':803 'handl':15,479,498,525 'handler':27,489,605,647 'hasmor':260 'haspermiss':738,935 'header':606,620 'http':26 'id':92,99,119,144,216,264,274,292,417,431,445,460,476,585,589,770,778,819 'identifi':58,948 'implement':17 'import':67,71,84,307,315,330,334,357,377,481,658 'index.ts':356 'inject':51,927 'input':125,874 'instanc':306,397,521,566,580 'instanceof':534 'interfac':723 'intern':470,543,553,559 'internalservererrorrespons':186,287,464,477 'invalid':643 'item':254 'itemschema':251,256 'json':811 'keyof':729 'limit':227,295 'listus':354 'logger':78 'low':33 'low-overhead':32 'maximum':232 'maxlength':107,138 'minimum':230 'minlength':105,136 'miss':645,762 'modul':721 'modular':298,899 'name':103,134,191,200 'new':168,173,641,691,702,709,760 'nextcursor':257 'node.js':38 'notfounderrorrespons':285,449,461,788 'npm':61 'number':682 'omit':127,881 'openapi':94,824 'operationid':161,266,772,830 'option':888 'output':878 'overhead':34 'pagin':223,246 'paginatedrespons':250 'param':275,776 'paramet':212 'paramsschema':214,276 'path':211 'pattern':209,615 'permiss':649,653,664,667,679,681,686,732,733,734,740,753,758,763,764 'plugin':29,47,922 'prefix':57,363,947 'prehandl':789,933 'prevalid':627 'problemdetail':382,387,408,423,437,451,466,857 'promis':747 'protect':938 'queri':220 'queryschema':225,278 'querystr':277 'rawreplydefaultexpress':310,325 'rawrequestdefaultexpress':311,324 'rawserverdefault':312,323 'read':669,694,705,712 'readon':708 'ref':582 'refer':44,575,592 'references/plugins.md':45,929 'references/typeid.md':52,950 'registr':300 'replac':634 'repli':189,290,496,630,745,795 'reply.status':203,508,537,549,800 'request':16,188,289,413,494,513,629,743,794 'request.body':193 'request.headers.authorization':633 'request.id':524,569 'request.log.error':546 'request.params':293 'request.params.id':798 'request.query':297 'request.token.permissions':750 'request.url':522,567 'request/response':89 'requir':813 'requiredpermiss':739,755 'respons':178,247,279,347,372,403,598,622,780,841 'rest':5 'return':202,352,507,536,548,741,799 'reusabl':572,827 'rfc':367,384,855 'role':651,684,728 'role-bas':650,683 'rolepermiss':689,731 'rout':14,150,299,375,594,767,836,900,939 'routes/users.ts':329 'safe':56,946 'schema':41,81,90,126,153,160,208,265,346,366,383,573,578,597,619,771,807,812,822,842,871,875,879 'see':928,949 'send':205,510,539,551,802 'server':471,544,554,560 'set':687,692,703,710 'setup':59 'share':574 'sinclair/typebox':65,88,333,380 'skill' 'skill-typescript-fastify' 'someth':563 'sort':239 'source-martinffx' 'specif':401 'split':873 'standard':370 'static':86,124,149,865 'status':391,414,428,442,457,473,514,556,845 'string':221,727,917 'structur':21 'summari':165,270,833 'tag':156,163,164,268,774,831 'throw':640,759 'time':118,910 'titl':393,516,558 'token':632,637,639,646,718,725 'topic-agent-skills' 'topic-agentic-coding' 'topic-anthropic' 'topic-claude-code' 'topic-claude-skills' 'topic-code-review' 'topic-codex' 'topic-codex-skill' 'topic-opencode' 'topic-prompt-engineering' 'topic-sdd' 'topic-spec-driven-development' 'traceid':399,523,568 'true':79 'type':55,85,122,147,195,304,320,331,378,389,410,425,439,453,468,511,552,659,678,869,893,897,945 'type-saf':54,944 'type.array':255,349 'type.boolean':261 'type.composite':407,422,436,450,465,862 'type.integer':229 'type.literal':242,244,411,415,426,429,440,443,454,458,469,474,915 'type.null':782 'type.number':392 'type.object':98,133,215,226,253,388,409,424,438,452,467,612,777,809 'type.optional':228,237,240,258,886 'type.ref':600,603 'type.string':100,104,110,114,135,141,217,238,259,390,394,396,398,400,614,779 'type.union':241,914 'typebox':20,40 'typeboxtypeprovid':72,316,327 'typeid':942 'typeof':680,730 'types.ts':302 'typescript':2,10,66,83,154,210,301,328,355,376,480,576,609,657,868 'typescript-fastifi':1 'unauthor':427 'unauthorizederror':642 'unauthorizederrorrespons':184,421,432,626,784 'uri':911 'usag':583,765 'use':11,369,529,854,864,885,913,920,932,941 'user':123,157,169,174,197,206,269,272,586,601,668,670,672,693,695,697,701,704,706,711,775,791 'userid':726 'userpermiss':749 'userpermissions.includes':757 'userrespons':120 'userrout':341,358,362 'userschema':97,180,281,350,587,624 'uuid':102,219,906 'v5':816 'valid':18,42,500,506,517,905 'verifytoken':638 'web':35 'went':564 'withtypeprovid':80 'work':24 'wrapper':248 'write':671,696,707 'wrong':565 'x':880","prices":[{"id":"1d189bbb-eecf-4940-9165-ce9f7d77f30c","listingId":"51d0f272-81a0-4ae3-9956-1299e395e56d","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"martinffx","category":"atelier","install_from":"skills.sh"},"createdAt":"2026-05-10T07:03:13.702Z"}],"sources":[{"listingId":"51d0f272-81a0-4ae3-9956-1299e395e56d","source":"github","sourceId":"martinffx/atelier/typescript-fastify","sourceUrl":"https://github.com/martinffx/atelier/tree/main/skills/typescript-fastify","isPrimary":false,"firstSeenAt":"2026-05-10T07:03:13.702Z","lastSeenAt":"2026-05-18T19:05:25.218Z"}],"details":{"listingId":"51d0f272-81a0-4ae3-9956-1299e395e56d","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"martinffx","slug":"typescript-fastify","github":{"repo":"martinffx/atelier","stars":23,"topics":["agent-skills","agentic-coding","anthropic","claude-code","claude-skills","code-review","codex","codex-skill","opencode","prompt-engineering","sdd","spec-driven-development"],"license":"mit","html_url":"https://github.com/martinffx/atelier","pushed_at":"2026-05-18T06:56:45Z","description":"An atelier for Opencode, Claude Code, and other coding agents: spec-driven workflows, deep thinking, and code quality.","skill_md_sha":"8e6610a781d5e9c72e5d2913b96c4fe74e58ba86","skill_md_path":"skills/typescript-fastify/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/martinffx/atelier/tree/main/skills/typescript-fastify"},"layout":"multi","source":"github","category":"atelier","frontmatter":{"name":"typescript-fastify","description":"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."},"skills_sh_url":"https://skills.sh/martinffx/atelier/typescript-fastify"},"updatedAt":"2026-05-18T19:05:25.218Z"}}