Back to Blog

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.

Viprasol Tech Team
18 min read
Updated 2026

fastify-openapi-glue: serviceHandlers & operationId Guide (2026)

TL;DR. fastify-openapi-glue is 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 the serviceHandlers option, and for each operationId declared in your spec, it calls the matching exported function in that module — turning operationId: listProjects into a call to serviceHandlers.listProjects(request, reply). Request and response validation against the spec is automatic. Below: working code, the exact serviceHandlers shape 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:

  1. Route registration — every path + method combination in your spec becomes a Fastify route. You don't write app.get('/projects', ...) by hand for every endpoint.
  2. Validation — request body, query parameters, path parameters, headers, and response shape are validated against your OpenAPI schemas using Ajv. Invalid requests get a 400 with field-level error details automatically.
  3. Handler dispatch — each route forwards to a function in your serviceHandlers module, keyed by the route's operationId.

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:

  1. Reads the operationId (for example listProjects).
  2. Looks up serviceHandlers[operationId] — must be a function.
  3. Registers a Fastify route at the matching path + method.
  4. 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.


fastify-openapi-glue - fastify-openapi-glue: serviceHandlers & operationId Guide (2026)

🚀 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 = operations[Op] extends { requestBody: { content: { 'application/json': infer B } } } ? B : never;

// Helper: extract path params type for an operationId type PathParams = operations[Op] extends { parameters: { path: infer P } } ? P : never;

// Strongly-typed serviceHandlers signature type ServiceHandlers = { [K in keyof operations]: ( request: FastifyRequest<{ Body: ReqBody; Params: PathParams; }>, reply: FastifyReply ) => Promise | unknown; };

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:

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

Featurefastify-openapi-gluefastify-swaggerexpress-openapi-validator
DirectionSpec → code (spec-driven)Code → spec (annotation-driven)Spec → validation (Express only)
Auto-mounts routesYesNo (you write routes)No (you write routes)
Request validationAutomatic via AjvVia separate Fastify validatorsAutomatic
Response validationYesNoYes
operationId requiredYesOptionalOptional
OpenAPI 3.1 supportFullFullFull
Server frameworkFastifyFastifyExpress
npm weekly downloads (May 2026)~140k~600k~250k
Best forSpec-driven projects, contract-first APIsExisting Fastify codebases needing docsExpress 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

  1. operationId is case-sensitive. listProjectslistprojects. The plugin throws FST_ERR_OAS_MISSING_HANDLER if cases do not match exactly.
  2. No duplicate operationIds across the entire spec. Even across different paths and methods, every operationId must be unique. The Redocly CLI linter catches this.
  3. serviceHandlers can 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.
  4. Missing operationId silently creates a generic name. If you forget operationId, the plugin generates one from method + path. This is brittle. Always set operationId explicitly.
  5. 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.
  6. additionalProperties defaults to true. To reject unknown fields in request bodies, set additionalProperties: false on each schema — or use the plugin option ajvOpts: { strict: true }.
  7. securityHandlers is a separate option. Authentication is not part of serviceHandlers. Pass securityHandlers: { 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:

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

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.

fastify-openapi-gluefastifyopenapioperationidservicehandlerstypescriptapi-design
Share this article:

About the Author

V

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.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

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

Viprasol · Web Development

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.