fastify-openapi-glue: serviceHandlers & operationId Guide (2026)
fastify-openapi-glue maps OpenAPI operationId to serviceHandlers in Fastify. Complete 2026 guide with npm install, working code, type defs, common gotchas.
fastify-openapi-glue: serviceHandlers & operationId Guide (2026)
TL;DR.
fastify-openapi-glueis the Fastify plugin that reads your OpenAPI 3.x specification and auto-mounts every route from it. You point it at a JS/TS module via theserviceHandlersoption, and for eachoperationIddeclared in your spec, it calls the matching exported function in that module — turningoperationId: listProjectsinto a call toserviceHandlers.listProjects(request, reply). Request and response validation against the spec is automatic. Below: working code, the exactserviceHandlersshape Fastify expects, TypeScript types, FAQ, and the gotchas that the npm-only docs don't mention.
API-first development inverts the typical workflow: instead of building an API and then documenting it, you write the OpenAPI specification first — then generate server types, client types, mock servers, and request validation from that single source of truth. In the Fastify ecosystem, fastify-openapi-glue (maintained by Hans Klunder) is the standard plugin for binding an OpenAPI spec to your Fastify server: it reads the spec, mounts every route, validates requests against schemas, and routes each call to a serviceHandlers function keyed by the spec's operationId. This guide covers fastify-openapi-glue in depth — the serviceHandlers contract, operationId mapping rules, TypeScript types, common pitfalls — alongside the broader OpenAPI 3.1 design patterns that make API-first development pay off.
What is fastify-openapi-glue?
fastify-openapi-glue is a Fastify plugin (npm: fastify-openapi-glue) that automatically generates HTTP routes from an OpenAPI 3.0 or 3.1 specification. As of May 2026, it is one of the most popular spec-driven plugins in the Fastify ecosystem and is maintained at github.com/seriousme/fastify-openapi-glue.
Three things it does for you:
- Route registration — every
path + methodcombination in your spec becomes a Fastify route. You don't writeapp.get('/projects', ...)by hand for every endpoint. - Validation — request body, query parameters, path parameters, headers, and response shape are validated against your OpenAPI schemas using Ajv. Invalid requests get a
400with field-level error details automatically. - Handler dispatch — each route forwards to a function in your
serviceHandlersmodule, keyed by the route'soperationId.
The contract is brutally simple: every operationId in your spec must have a corresponding function in serviceHandlers. If the spec defines operationId: listProjects, your handlers module must export listProjects. Miss one and the plugin throws FST_ERR_OAS_MISSING_HANDLER at startup — exactly the discipline you want for a spec-driven API.
How serviceHandlers Maps operationId to Route Handlers
Here is the exact mapping fastify-openapi-glue performs at registration time:
```yaml
openapi.yaml
paths: /projects: get: operationId: listProjects # ← this is the key summary: List projects responses: '200': { description: OK } ```
```js // serviceHandlers module export default { // Function name MUST match the operationId exactly (case-sensitive) async listProjects(request, reply) { const projects = await db.projects.findMany(); return { data: projects, meta: { total: projects.length } }; }, }; ```
The plugin walks the spec at startup, and for each operation object:
- Reads the
operationId(for examplelistProjects). - Looks up
serviceHandlers[operationId]— must be a function. - Registers a Fastify route at the matching
path + method. - Wraps the handler with auto-generated Ajv validators for request body, query, params, headers, and response.
If operationId is missing on a path operation, the plugin falls back to a generated name like getProjects (method + path). This is fragile — always set operationId explicitly in production specs.
If the function is missing from serviceHandlers, the plugin throws FST_ERR_OAS_MISSING_HANDLER at startup. This is intentional — it forces 1:1 sync between spec and code.
🌐 Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different — senior engineers only, direct Slack access, and a 5.0★ Upwork record across 1000+ projects.
- React, Next.js, Node.js, TypeScript — production-grade stack
- Fixed-price contracts — no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
Installation
```bash npm install fastify fastify-openapi-glue
add YAML parsing if your spec is YAML rather than JSON
npm install js-yaml ```
Versions current as of May 2026: fastify ^5.x, fastify-openapi-glue ^5.0.0. The plugin supports OpenAPI 3.0.x and 3.1.x.
Working Example: Multiple Operations
Here is a complete, runnable example with three operations — listProjects, createProject, getProject — using a single serviceHandlers module.
openapi.yaml:
```yaml openapi: 3.1.0 info: { title: Projects API, version: 1.0.0 } paths: /projects: get: operationId: listProjects responses: '200': { description: OK } post: operationId: createProject requestBody: required: true content: application/json: schema: type: object required: [name] properties: name: { type: string, minLength: 1, maxLength: 255 } responses: '201': { description: Created } /projects/{projectId}: get: operationId: getProject parameters: - name: projectId in: path required: true schema: { type: string, format: uuid } responses: '200': { description: OK } '404': { description: Not Found } ```
src/handlers.js (every operationId from the spec needs a matching export):
```js export default { async listProjects(request, reply) { return { data: await db.projects.findMany() }; },
async createProject(request, reply) { // request.body is already validated against the spec const project = await db.projects.create({ data: request.body }); reply.code(201); return project; },
async getProject(request, reply) { // request.params.projectId is validated as uuid by the spec const project = await db.projects.findUnique({ where: { id: request.params.projectId } }); if (!project) { reply.code(404); return { code: 'NOT_FOUND', message: 'Project not found' }; } return project; }, }; ```
src/server.js:
```js import Fastify from 'fastify'; import openapiGlue from 'fastify-openapi-glue'; import handlers from './handlers.js';
const app = Fastify({ logger: true });
app.register(openapiGlue, { specification: './openapi.yaml', serviceHandlers: handlers, // pass the object directly... // ...or pass a path: serviceHandlers: './src/handlers.js' prefix: '/v1', // optional URL prefix for all routes });
app.listen({ port: 3000 }); ```
That is it. All three routes are mounted, validated, and dispatched. No app.get/post/... calls anywhere.

🚀 Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4–8 weeks, full platforms in 3–5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
TypeScript Types for serviceHandlers
fastify-openapi-glue ships ambient types, but serviceHandlers is typed loosely as Record<string, RouteHandlerMethod>. For strict typing, generate types from your spec with openapi-typescript and constrain serviceHandlers against them:
```ts import type { FastifyRequest, FastifyReply } from 'fastify'; import type { operations } from './types/api.d.ts'; // generated from openapi.yaml
// Helper: extract request body type for an operationId
type ReqBody
// Helper: extract path params type for an operationId
type PathParams
// Strongly-typed serviceHandlers signature
type ServiceHandlers = {
[K in keyof operations]: (
request: FastifyRequest<{
Body: ReqBody
const handlers: ServiceHandlers = { async listProjects(request, reply) { /* typed */ }, async createProject(request, reply) { const { name } = request.body; // typed from spec return db.projects.create({ data: { name } }); }, async getProject(request, reply) { const { projectId } = request.params; // typed as uuid string return db.projects.findUnique({ where: { id: projectId } }); }, };
export default handlers; ```
This makes the TypeScript compiler enforce the spec-to-handler contract — drop an operationId from the spec, and the matching key vanishes from keyof operations, breaking the build.
OpenAPI 3.1 Specification Structure
Below is a complete OpenAPI 3.1 specification that the fastify-openapi-glue examples above bind to. This is the broader OpenAPI 3.1 reference for the spec itself — schemas, security, paths, and reusable components.
```yaml
openapi.yaml
openapi: 3.1.0
info: title: Acme API version: 1.0.0 description: | REST API for Acme SaaS platform.
## Authentication
All endpoints require Bearer token authentication.
Obtain tokens via POST /auth/login.
servers:
- url: https://api.yourapp.com/v1 description: Production
- url: https://api.staging.yourapp.com/v1 description: Staging
- url: http://localhost:3000/v1 description: Local development
components: securitySchemes: BearerAuth: type: http scheme: bearer bearerFormat: JWT
schemas: UUID: type: string format: uuid example: "550e8400-e29b-41d4-a716-446655440000"
Timestamp:
type: string
format: date-time
example: "2026-05-23T10:30:00Z"
Error:
type: object
required: [code, message]
properties:
code: { type: string, example: "VALIDATION_ERROR" }
message: { type: string, example: "The request body is invalid" }
details:
type: array
items:
type: object
properties:
field: { type: string }
message: { type: string }
PaginatedMeta:
type: object
required: [total, page, perPage, totalPages]
properties:
total: { type: integer, example: 142 }
page: { type: integer, example: 1 }
perPage: { type: integer, example: 20 }
totalPages: { type: integer, example: 8 }
Project:
type: object
required: [id, name, status, createdAt, updatedAt]
properties:
id: { $ref: '#/components/schemas/UUID' }
name: { type: string, minLength: 1, maxLength: 255 }
description: { type: string, nullable: true, maxLength: 5000 }
status: { type: string, enum: [active, archived, deleted] }
memberCount: { type: integer, minimum: 0 }
createdAt: { $ref: '#/components/schemas/Timestamp' }
updatedAt: { $ref: '#/components/schemas/Timestamp' }
CreateProjectRequest:
type: object
required: [name]
properties:
name: { type: string, minLength: 1, maxLength: 255 }
description: { type: string, maxLength: 5000 }
security:
- BearerAuth: []
paths: /projects: get: operationId: listProjects summary: List projects tags: [Projects] parameters: - { name: page, in: query, schema: { type: integer, minimum: 1, default: 1 } } - { name: perPage, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } } - { name: status, in: query, schema: { type: string, enum: [active, archived] } } responses: '200': description: Paginated list of projects content: application/json: schema: type: object required: [data, meta] properties: data: { type: array, items: { $ref: '#/components/schemas/Project' } } meta: { $ref: '#/components/schemas/PaginatedMeta' } '401': description: Unauthorized content: application/json: schema: { $ref: '#/components/schemas/Error' }
post:
operationId: createProject
summary: Create a project
tags: [Projects]
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/CreateProjectRequest' }
responses:
'201':
description: Project created
content:
application/json:
schema: { $ref: '#/components/schemas/Project' }
'400':
description: Validation error
content:
application/json:
schema: { $ref: '#/components/schemas/Error' }
```
TypeScript Code Generation
Generate types from your OpenAPI spec — never write API types manually:
```bash npm install -D openapi-typescript npx openapi-typescript openapi.yaml -o src/types/api.d.ts ```
```ts // src/types/api.d.ts (generated — do not edit manually) export interface paths { '/projects': { get: operations['listProjects']; post: operations['createProject']; }; '/projects/{projectId}': { get: operations['getProject']; }; }
export type operations = { listProjects: { parameters: { query?: { page?: number; perPage?: number; status?: 'active' | 'archived' } }; responses: { 200: { content: { 'application/json': { data: Project[]; meta: PaginatedMeta } } }; }; }; createProject: { /* ... */ }; }; ```
Used together with the typed serviceHandlers snippet above, this makes both client and server type-safe end to end.
fastify-openapi-glue vs fastify-swagger vs express-openapi-validator
| Feature | fastify-openapi-glue | fastify-swagger | express-openapi-validator |
|---|---|---|---|
| Direction | Spec → code (spec-driven) | Code → spec (annotation-driven) | Spec → validation (Express only) |
| Auto-mounts routes | Yes | No (you write routes) | No (you write routes) |
| Request validation | Automatic via Ajv | Via separate Fastify validators | Automatic |
| Response validation | Yes | No | Yes |
operationId required | Yes | Optional | Optional |
| OpenAPI 3.1 support | Full | Full | Full |
| Server framework | Fastify | Fastify | Express |
| npm weekly downloads (May 2026) | ~140k | ~600k | ~250k |
| Best for | Spec-driven projects, contract-first APIs | Existing Fastify codebases needing docs | Express teams migrating to spec-first |
Use fastify-openapi-glue when: you write the OpenAPI spec first and want the server to follow it. Use fastify-swagger when: you have an existing Fastify app and want generated documentation from your code. Use express-openapi-validator when: you are on Express, not Fastify.
Common Gotchas with serviceHandlers and operationId
- operationId is case-sensitive.
listProjects≠listprojects. The plugin throwsFST_ERR_OAS_MISSING_HANDLERif cases do not match exactly. - No duplicate operationIds across the entire spec. Even across different paths and methods, every
operationIdmust be unique. The Redocly CLI linter catches this. serviceHandlerscan be a path string or an object. Passing the imported object directly (serviceHandlers: handlers) is more explicit and works better with bundlers than passing a path string.- Missing operationId silently creates a generic name. If you forget
operationId, the plugin generates one from method + path. This is brittle. Always setoperationIdexplicitly. - Response validation is strict. If your handler returns a field the spec does not declare, the response is rejected. Either remove the field or update the spec.
additionalPropertiesdefaults totrue. To reject unknown fields in request bodies, setadditionalProperties: falseon each schema — or use the plugin optionajvOpts: { strict: true }.securityHandlersis a separate option. Authentication is not part ofserviceHandlers. PasssecurityHandlers: { BearerAuth: async (request) => verifyToken(request) }alongside.
Request Validation Middleware
Validate all incoming requests against your OpenAPI spec automatically. With fastify-openapi-glue this is built in — but for reference, here is the manual approach using Ajv directly:
```ts import Ajv from 'ajv'; import addFormats from 'ajv-formats'; import yaml from 'js-yaml'; import { readFileSync } from 'fs';
const spec = yaml.load(readFileSync('./openapi.yaml', 'utf8')) as Record<string, unknown>;
const ajv = new Ajv({ allErrors: true, coerceTypes: true }); addFormats(ajv);
const validateCreateProject = ajv.compile({ type: 'object', required: ['name'], properties: { name: { type: 'string', minLength: 1, maxLength: 255 }, description: { type: 'string', maxLength: 5000 }, }, additionalProperties: false, });
app.post('/v1/projects', async (request, reply) => { if (!validateCreateProject(request.body)) { return reply.code(400).send({ code: 'VALIDATION_ERROR', message: 'The request body is invalid', details: validateCreateProject.errors?.map(e => ({ field: e.instancePath.replace('/', ''), message: e.message, })), }); } // route handler... }); ```
In practice, prefer fastify-openapi-glue over manual Ajv — the plugin gives you validation, route registration, and handler dispatch in one step.
Contract Testing with Prism
Prism runs a mock server from your OpenAPI spec — frontend teams can develop before the backend exists, and you can validate that your real server conforms to the spec.
```bash npm install -g @stoplight/prism-cli
Start mock server
prism mock openapi.yaml --port 4010
Validate that your real server conforms to the spec
prism proxy openapi.yaml http://localhost:3000 --port 4020
Sends requests to real server, validates responses match spec
Reports any schema violations
```
Keeping Spec and Code in Sync
```yaml
.github/workflows/api-spec.yml
name: API Spec Validation
on: [pull_request]
jobs: validate-spec: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Validate OpenAPI spec
run: npx @redocly/cli lint openapi.yaml
- name: Regenerate types
run: npx openapi-typescript openapi.yaml -o src/types/api.gen.d.ts
- name: Check for type drift
run: git diff --exit-code src/types/api.d.ts
- name: Contract tests
run: |
npx prism mock openapi.yaml --port 4010 &
sleep 2
npm run test:contract
```
API Versioning Strategy
```yaml servers:
- url: https://api.yourapp.com/v1 description: Current stable API
- url: https://api.yourapp.com/v2 description: Next version (beta) ```
Header-based versioning is the alternative: all requests to /api/* carry API-Version: 2026-05 — Stripe uses this pattern.
FAQ: fastify-openapi-glue
How does serviceHandlers map operationId in fastify-openapi-glue?
The plugin reads each operationId from the OpenAPI spec at startup and looks up a function with the same name on the serviceHandlers object. If the spec has operationId: listProjects, the handler must export a function named listProjects(request, reply). Names are case-sensitive and must be unique across the entire spec. Missing handlers throw FST_ERR_OAS_MISSING_HANDLER at registration.
What is serviceHandlers in fastify-openapi-glue?
serviceHandlers is either a JavaScript module path or an object containing one function per OpenAPI operationId. Each function receives (request, reply) and is called when a request matches the corresponding spec operation. Request body, query, path params, and headers are already validated against the spec by the time your function runs.
How do I install fastify-openapi-glue?
Run npm install fastify fastify-openapi-glue. Then register the plugin: app.register(fastifyOpenapiGlue, { specification: './openapi.yaml', serviceHandlers: './src/handlers.js' }). The plugin supports Fastify 4.x and 5.x as of May 2026.
Does fastify-openapi-glue support OpenAPI 3.1?
Yes. It supports OpenAPI 3.0.x and 3.1.x. OpenAPI 3.1 features such as JSON Schema 2020-12 compatibility and nullable type unions (type: [string, null]) work out of the box.
How do I add TypeScript types to serviceHandlers?
Generate types from your spec with openapi-typescript, then constrain serviceHandlers to { [K in keyof operations]: (req: FastifyRequest<...>, reply: FastifyReply) => unknown }. See the TypeScript section above for the full type definition.
Why is fastify-openapi-glue throwing FST_ERR_OAS_MISSING_HANDLER?
The plugin cannot find a function matching one of your spec's operationId values on the serviceHandlers object. Check: (a) the function is exported, (b) the name matches exactly (case-sensitive), (c) the spec actually has operationId set on that operation.
Can fastify-openapi-glue auto-generate documentation?
No — it is spec-driven, so the spec is your documentation. Serve it with @fastify/swagger-ui or Redoc for a browsable UI.
Partnering With Viprasol
We design APIs with OpenAPI-first methodology — specification design, TypeScript code generation, validation middleware, mock servers for parallel development, and documentation hosted with Redoc or Swagger UI. Well-designed APIs accelerate frontend development and reduce integration errors.
→ Talk to our team about API design and documentation.
Continue Learning
- API Versioning Strategies — managing API evolution
- GraphQL vs REST vs gRPC — choosing the right API paradigm
- API Documentation Tools — tools for publishing API docs
- TypeScript Advanced Patterns — TypeScript for API type safety
- Web Development Services — API design and development
npm fastify-openapi-glue serviceHandlers operationId Example, Explained
When you install the npm fastify-openapi-glue serviceHandlers operationId workflow, the plugin maps each route in your OpenAPI document to a method on your service object using the operationId field. The connection is purely name-based: an operation named getUserById in your spec must resolve to a getUserById function in your serviceHandlers class or object. If the names do not match, the route silently returns a 501 Not Implemented, which is the most common pitfall teams hit.
A clean fastify-openapi-glue serviceHandlers operationId example keeps every operationId unique, descriptive, and valid as a JavaScript identifier, then exposes a matching handler that receives the standard Fastify request and reply. This keeps your API contract and implementation in lockstep. At Viprasol Tech, our senior engineers take full ownership of designing that spec-to-handler mapping so your routes, validation, and operationId bindings stay consistent as the API grows.
External Resources
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 1000+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement.
Need a Modern Web Application?
From landing pages to complex SaaS platforms — we build it all with Next.js and React.
Free consultation • No commitment • Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse ≥90 scores, mobile-first design, and full source code ownership. Senior engineers only — from architecture through deployment.