{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreiggcle5vcitwxe4cbrfivphy527x2hztzicsay6vfujjsaror2rsq",
"uri": "at://did:plc:hj676ljmxzoweeryyejoaehh/app.bsky.feed.post/3mlvh226qvdi2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreieaendg74oxccexkbspyb63oqwgftruobyzac2eduutaer46i2eza"
},
"mimeType": "image/webp",
"size": 116422
},
"description": "Fastify is a fantastic, popular, low-overhead Node.js framework, but how does the support for OpenAPI stack up?",
"path": "/blog/fastifys-openapi-plugins-which-are-best/",
"publishedAt": "2026-05-15T13:32:49.000Z",
"site": "https://apisyouwonthate.com",
"tags": [
"Annotations, Comments, or Decorators",
"OpenAPI-aware Frameworks",
"OpenAPI Middlewares",
"server-side validation middleware",
"Swagger (OpenAPI v2)",
"OpenAPI v3",
"OpenAPI ecosystem",
"commits like this",
"Server-Side Validation with API DescriptionsValidation can mean a lot of things, but in API land it generally means figuring out if the data being sent to the API is any good or not. Validation can happen in a lot of different places - it can happen on the server, and it can happen inAPIs You Won't HatePhil Sturgeon",
"Scalar's API Reference docs",
"@fastify/autoload",
"http://localhost:3000/reference",
"not actually compatible",
"get them lined up in OpenAPI v3.1",
"json-schema-to-openapi-schema",
"fastify-openapi-glue",
"seriousme",
"fastify-swaggergen",
"somewhat",
"GitHub - eropple/fastify-openapi3: Developer-friendly OpenAPI3 tooling for Fastify that’s easy to use.Developer-friendly OpenAPI3 tooling for Fastify that’s easy to use. - eropple/fastify-openapi3GitHuberopple",
"@sinclair/typebox",
"looks pretty good",
"wrapper",
"Fastify Ecosystem",
"OpenAPI.Tools",
"GitHub - PayU/openapi-validator-middleware: Input validation using Swagger (Open API) and ajvInput validation using Swagger (Open API) and ajv. Contribute to PayU/openapi-validator-middleware development by creating an account on GitHub.GitHubPayU",
"openapi-data-validator.js",
"Warren Parad",
"Scalar",
"Github Action",
"@fastify",
"@scalar"
],
"textContent": "Fastify took the JavaScript community by storm in 2016, offering a solid alternative to things like Express which were starting to feel a bit rough around the edges in a world where demands for APIs were growing, and HTTP was evolving rapidly.\n\nOne of the core features is JSON Schema validated routing, so several times over the last decade of wanting to find frameworks with good support for OpenAPI I have wandered over, and been let down by what was on offer.\n\nThere are three main ways API frameworks can integrate with OpenAPI.\n\n * Annotations, Comments, or Decorators - Old school code-first approach of sprinkling some extra syntax around your code and hope that proximity leads to accuracy, which it generally doesn't.\n * OpenAPI-aware Frameworks - The framework is creating OpenAPI simply from the actual bare bones of the code, so when you register a `app.post('/foo/{id}')` and all the rest of the validation and logic, it will create that OpenAPI for you. The modern way to handle code-first.\n * OpenAPI Middlewares - The secret power of design-first is being able to reference that `openapi.yaml` in a server-side validation middleware that uses the OpenAPI to provide powerful request validation (and often response contract testing) to reduce you writing all that code a second and third time.\n\n\n\nWhich of these approaches does Fastify offer between official and community plugins?\n\nWe're going to look at the following:\n\n 1. `@fastify/swagger`\n 2. `fastify-openapi-glue`\n 3. `eropple/fastify-openapi3`\n 4. Something I just slapped together in five minutes using `openapi-data-validator.js`.\n\n\n\n## Package 1: @fastify/swagger\n\nThe official plugin for Fastify, so the first place many will look.\n\n> A Fastify plugin for serving Swagger (OpenAPI v2) or OpenAPI v3 schemas, which are automatically generated from your route schemas, or an existing Swagger/OpenAPI schema.\n\nThe official `@fastify/swagger` plugin starts off showing its age just in the name. Swagger is the long dead name of OpenAPI, now only living on as a trademark for an ageing suite of SmartBear products, and generally abandoned by every modern tool in the OpenAPI ecosystem.\n\nThe tool mentions supporting \"v2 or v3\", and usually that means it does not support v3.1 or v3.2. Thankfully after some digging around it does seem to support OpenAPI v3.1, but I had to scrabble around in the code looking for commits like this to find that out as it's just not been mentioned on the repository.\n\nDigging into the functionality now, there are two modes: \"dynamic\" and \"static\". Dynamic is where most of the functionality and documentation lays so let's start with that.\n\n### **Dynamic mode - old school code-first**\n\nHere is the code sample from the README.\n\n\n const fastify = require('fastify')()\n\n await fastify.register(require('@fastify/swagger'), {\n openapi: {\n openapi: '3.0.0',\n info: {\n title: 'Test swagger',\n description: 'Testing the Fastify swagger API',\n version: '0.1.0'\n },\n tags: [\n { name: 'user', description: 'User related end-points' },\n { name: 'code', description: 'Code related end-points' }\n ],\n components: {\n securitySchemes: {\n apiKey: {\n type: 'apiKey',\n name: 'apiKey',\n in: 'header'\n }\n }\n }\n }\n })\n\n fastify.put('/some-route/:id', {\n schema: {\n description: 'post some data',\n tags: ['user', 'code'],\n summary: 'qwerty',\n security: [{ apiKey: [] }],\n params: {\n type: 'object',\n properties: {\n id: {\n type: 'string',\n description: 'user id'\n }\n }\n },\n body: {\n type: 'object',\n properties: {\n hello: { type: 'string' },\n obj: {\n type: 'object',\n properties: {\n some: { type: 'string' }\n }\n }\n }\n },\n response: {\n 201: {\n description: 'Successful response',\n type: 'object',\n properties: {\n hello: { type: 'string' }\n }\n },\n default: {\n description: 'Default response',\n type: 'object',\n properties: {\n foo: { type: 'string' }\n }\n }\n }\n }\n }, (req, reply) => { })\n\n await fastify.ready()\n fastify.swagger()\n\nGeneral OpenAPI metadata like `info`, `servers`, and `tags` are defined in the middleware registration, then the schema objects are defined on the route.\n\nThe only thing OpenAPI-specific about this is the middleware registration, and the \"write all the schemas in the routes file\" experience is just how the JSON Schema validation already works in Fastly core.\n\nThat means people who have never even heard of OpenAPI are already defining these schemas on their routes to get benefits like server-side JSON Schema validation.\n\nServer-Side Validation with API DescriptionsValidation can mean a lot of things, but in API land it generally means figuring out if the data being sent to the API is any good or not. Validation can happen in a lot of different places - it can happen on the server, and it can happen inAPIs You Won't HatePhil Sturgeon\n\nDefining all the schemas in the routes, regardless of whether that's a framework convention or not, puts it into the _Annotations, Comments, or Decorators_ category.\n\nYes those schemas will validate incoming HTTP requests, but the juice is really not worth the squeeze if this is how you have to do it. The cumbersome approaching of having it all in one routes file is going to lead to conflicts, and whilst you can DRY it up, mixing in $ref with JavaScript code is just getting weird. You cannot rely on linters like Spectral or Vacuum to help check if any of its even valid, let alone set up your own API style guides to sniff out problems during development phase.\n\nWorking like this is ok for anyone who would rather use JSON Schema to Joi for example, but for the OpenAPI community it's inappropriate for anyone doing anything more than checking a box of \"making some OpenAPI docs\".\n\nThere are two extra plugins to add to get docs out of this OpenAPI. One is the prehistoric Swagger UI that's mainly being updated by a slopbot, and the other is Scalar's API Reference docs which is infinitely more modern, and being actively developed by human beings.\n\n\n npm install @scalar/fastify-api-reference\n\nWedge it into the routes somewhere. For me this was in `plugins/openapi.js` which is picked up by @fastify/autoload.\n\n\n import fp from \"fastify-plugin\";\n import swagger from \"@fastify/swagger\";\n import scalar from \"@scalar/fastify-api-reference\";\n\n export default fp(async (fastify) => {\n await fastify.register(swagger, {\n openapi: {\n info: {\n title: \"Train Travel API\",\n description: \"API for finding and booking train trips across Europe.\",\n # ... snip ...\n }\n },\n });\n\n await fastify.register(scalar, {\n routePrefix: \"/reference\",\n });\n });\n\n\nVery simple to add the last few lines, and thanks to some sort of magic with a hidden routing registry, the `scalar/fastify-api-reference` middleware has everything it needs from `fastify/swagger` to build out as much documentation as it can from the metadata, annotations, and schema provided in the source code.\n\nScalar documentation hosted on the same server as Fastify endpoints, which in this case was http://localhost:3000/reference.\n\nThis is simply done, but looking a bit sparse, so a lot more keywords and descriptions will need to be thrust into the routes code to make it actually useful as documentation.\n\n### **Static mode - load in openapi.yaml**\n\nIf dynamic mode makes you define everything in the routes file, and static lets you load in existing OpenAPI, is that going to help me validate requests from my existing fantastic `openapi.yaml`?\n\nHere's the code that shows how to make Fastify aware of an existing `openapi.yaml` that is quite rightly in the source code, and pass it off to Scalar to render beautifully.\n\n\n const fastify = require(\"fastify\")({ logger: true });\n const swagger = require(\"@fastify/swagger\");\n const scalar = require(\"@scalar/fastify-api-reference\");\n\n fastify.register(swagger, {\n mode: \"static\",\n specification: {\n path: \"./openapi.yaml\",\n },\n });\n\n fastify.register(scalar, {\n routePrefix: \"/reference\",\n configuration: {\n title: \"API Reference\",\n },\n });\n\n fastify.post(\"/bookings\", async (request, reply) => {});\n fastify.post(\"/stations\", async (request, reply) => {});\n fastify.post(\"/trips\", async (request, reply) => {});\n\n fastify.listen({ port: 3000 }, (err) => {\n if (err) throw err;\n });\n\n\nRun that server and the `localhost:3000/reference` is now powered by the original `openapi.yaml` which has been through all sorts of tech writers (who didn't fancy mucking about learning JavaScript to contribute), so the docs are infinitely better fleshed out.\n\nSadly, this does not provide any request validation. It just serves the docs, and that is all.\n\nOnce again, this comes down to the JSON Schema validation being a core feature of Fastify itself. They are not reading the OpenAPI and creating temporary schema/validation objects at build time or run time, which could then be used to validate just like if they were written all over the routes file. This one package could support design-first and code-first nicely if it just did that, but... it does not.\n\n### Conflating OpenAPI Schema and JSON Schema\n\nThroughout all of this there are alarm bells ringing around using JSON Schema by default, then trying to shove that into OpenAPI v3.0. Without boring you all with years of trouble, the two schema objects are not actually compatible! OpenAPI v3.0 schemas were a subset _and_ a superset. It took a lot of work to get them lined up in OpenAPI v3.1.\n\nUnless Fastify leverage something like json-schema-to-openapi-schema and/or nudge people towards OpenAPI v3.1+, then this is going to be confusing. That confusion is split between everyone who just hasn't got around to noticing the problems yet, and the even more confusing situation of when the mistakes pop up.\n\n## Package 2: fastify-openapi-glue\n\nSo, Fastify wants somebody or something to go and build those Schema objects ey?\n\nThankfully that's exactly the sort of thing computers are good at, so up steps fastify-openapi-glue by seriousme. Right off the bat, there's a lot to like.\n\n 1. \"It aims at facilitating \"design first\" API development.\n 2. This project replaces fastify-swaggergen to focus on the future of OpenAPI not old timey Swagger.\n 3. It seems to support (or at least accept) OpenAPI v3.1 somewhat.\n 4. It will build your schema objects so you don't have to!\n\n\n\nThis all sounds a bit good to be true, so let's look at the code real quick then see what the validation looks like.\n\n\n import Fastify from \"fastify\";\n import scalar from \"@scalar/fastify-api-reference\";\n import openapiGlue from \"fastify-openapi-glue\";\n import fs from \"node:fs\";\n import path from \"node:path\";\n import { fileURLToPath } from \"node:url\";\n import { Service } from \"./service.js\";\n import { Security } from \"./security.js\";\n\n const fastify = Fastify({\n logger: true,\n ajv: {\n customOptions: {\n strict: false, // Allow custom formats without throwing errors\n },\n },\n });\n const currentDir = path.dirname(fileURLToPath(import.meta.url));\n const openApiPath = `${currentDir}/openapi.yaml`;\n const openApiContent = fs.readFileSync(openApiPath, \"utf8\");\n\n await fastify.register(openapiGlue, {\n specification: openApiPath,\n serviceHandlers: new Service(),\n securityHandlers: new Security(),\n });\n\n await fastify.register(scalar, {\n routePrefix: \"/reference\",\n configuration: {\n title: \"API Reference\",\n content: openApiContent,\n },\n });\n\n await fastify.listen({ port: 3000 });\n\n\nPretty simple on the face of it. Point it at the `openapi.yaml` and something about running AJV in not-so-strict mode all fine, and Scalar API Reference is there again to show off the docs which is optional.\n\nMore light needs to be shed on services and security handlers in a minute, but at this point let's see how validation works.\n\nLet's fire off an invalid `POST /bookings` request, that has an integer for `trip_id` instead of the UUID.\n\n\n curl -s -X POST http://localhost:3000/bookings \\\n -H \"Content-Type: application/json\" \\\n -d '{\"trip_id\": 123, \"passenger_name\": \"John Doe\"}' | jq .\n\n\nResponse:\n\n\n {\n \"statusCode\": 400,\n \"code\": \"FST_ERR_VALIDATION\",\n \"error\": \"Bad Request\",\n \"message\": \"body/trip_id must match format \\\"uuid\\\"\"\n }\n\nSending in a real UUID however gets through the validation happily.\n\n\n curl -s -X POST http://localhost:3000/bookings \\\n -H \"Content-Type: application/json\" \\\n -d '{\"trip_id\": \"4f4e4e1c-c824-4d63-b37a-d8d698862f1d\", \"passenger_name\": \"John Doe\", \"has_bicycle\": true}' | jq .\n\n\nResponse:\n\n\n {\n \"id\": \"35f7f686-1b78-4489-9e2b-a4d9c07cd08c\",\n \"trip_id\": \"4f4e4e1c-c824-4d63-b37a-d8d698862f1d\",\n \"passenger_name\": \"John Doe\",\n \"has_bicycle\": true,\n \"has_dog\": false,\n \"links\": {\n \"self\": \"http://localhost:3000/bookings/35f7f686-1b78-4489-9e2b-a4d9c07cd08c\"\n }\n }\n\nVery nice, thank you for that. Saves me writing up 1000 if conditions or turning OpenAPI into `addSchema`, Joi, or any one of a thousand other \"rewriting your contract out again and again\" situations most of us API developers are trying to avoid.\n\nIs it just for `POST`? Nope! Handles `GET` and all the other HTTP methods nicely too. Let's see what happens if we just ask for all train trips in all or Europe without any query parameters like a city to go to or from.\n\n\n curl -s \"http://localhost:3000/trips\" | jq .\n\nThe response:\n\n\n {\n \"statusCode\": 400,\n \"code\": \"FST_ERR_VALIDATION\",\n \"error\": \"Bad Request\",\n \"message\": \"querystring must have required property 'origin'\"\n }\n\n\nThank you! Fantastic.\n\nThe one thing I glossed over to get to the functionality was the security and service classes.\n\nThe security handler is simple enough.\n\n\n export class Security {\n async OAuth2(req, scopes, schema) {\n // Demo-only auth: allow all requests so you can focus on validation behavior.\n return true;\n }\n }\n\nIt's easy enough to imagine checking some stuff and returning true or false in there. What about the services?\n\nUsing the build in open-glue CLI command, you pass it an OpenAPI document and it generates a whole project folder for you, with empty stubs in the service handler and loads of huge comments that contain all the YAML of the OpenAPI that was passed;.\n\nA few problems immediately show up with this approach.\n\n 1. When I add more endpoints I cannot run this again without overriding code.\n 2. The stub code being generated has syntax errors.\n\n\n\n 1. I don't want to do any of this!\n\n\n\nWhen one plugin decides that it is going to throw out all the usual way of working, it's a big jolt to the team who are expected to work with it.\n\nSuddenly the team of experienced Fastify developers are not writing Fastify routes and middlewares, they're writing \"Fastify OpenAPI Glue Service handlers\".\n\nWhat if they want to use some other plugin which hijacks things similarly extremely?\n\nThis might not be a blocker for you, but it would be for me. I just want to let the OpenAPI that's already been defined handle request validation (and ideally response contract testing) in the framework as a vanilla experience instead of inventing a whole new paradigm inside the framework.\n\n## Package 3: eropple/fastify-openapi3\n\nA third-party extension has popped up that looks to replace fastify/swagger with a more modern-focused code-first system \"whack your schema in the routes\" approach, which advertises OpenAPI v3.1 support right off the bat.\n\nGitHub - eropple/fastify-openapi3: Developer-friendly OpenAPI3 tooling for Fastify that’s easy to use.Developer-friendly OpenAPI3 tooling for Fastify that’s easy to use. - eropple/fastify-openapi3GitHuberopple\n\nThis library does take a few opinionated stances, such as requiring the use @sinclair/typebox.\n\n## Honourable Mentions\n\n### Hey-API\n\nThe Fastify integration from new popular SDK generator Hey-API looks pretty good at first...\n\n\n const fastify = Fastify();\n const serviceHandlers: RouteHandlers = {\n createPets(request, reply) {\n reply.code(201).send();\n },\n listPets(request, reply) {\n reply.code(200).send([]);\n },\n showPetById(request, reply) {\n reply.code(200).send({\n id: Number(request.params.petId),\n name: 'Kitty',\n });\n },\n };\n fastify.register(glue, { serviceHandlers });\n\n... but it's just a wrapper around `fastify-openapi-glue` and still seems to want you to have a one-off generation of handlers which are non-standard Fastify.\n\n### PayU/openapi-validator-middleware\n\nThis is the only other entry on the Fastify Ecosystem page which mentions OpenAPI is `PayU/openapi-validator-middleware`, and sadly this is a tool I had to kick off OpenAPI.Tools years ago for inactivity. The last release was Feb 28, 2022, and the tool does not support v3.1.\n\nThe last supported version of Fastify was v3, and we are in a v5 world now.\n\nGitHub - PayU/openapi-validator-middleware: Input validation using Swagger (Open API) and ajvInput validation using Swagger (Open API) and ajv. Contribute to PayU/openapi-validator-middleware development by creating an account on GitHub.GitHubPayU\n\nIt's a shame because the approach was exactly what is actually needed, and no more.\n\nThe requests are validated against OpenAPI in middleware, letting Fastify developers continue to write their code like Fastify developers do, only without needing to spam OpenAPI-kinda-but-not-really all over the source code, and keeping the OpenAPI development out of the way so that Text/GUI editors can be used to manage it, source code can leverage it, contract testing can use it across the. test suite, and if you really need to pull in docs and emit them from the API you can.\n\n## Conclusion\n\nObviously a lot of hard work has gone into all of these tools over the years, and OpenAPI is not an easy space for individual developers or small teams to keep up with (especially with OpenAPI v3.1 introducing breaking changes...) but this usually just means we need better collaboration and more reliance on shared utility packages instead of everyone reinventing the sausage single individually.\n\nBased on whats on offer, if you prefer code-first maybe see how far you can get with `@fastify/swagger` or `eropple/fastify-openapi3`.\n\nIf you're design-first, maybe the approach taken by `fastify-openapi-glue` is not so bad once you're used to it?\n\nWait, I have an idea,\n\n## Package 4: Slap a middleware together yourself\n\nIf you agree that all you really need is a middleware, there are quite a few out there which are not listed on the Fastify Ecosystem page, but could work either out of the box or with a bit of tinkering.\n\nI've got my eye on openapi-data-validator.js from Authress Engineering and friend of the community Warren Parad.\n\n\n curl -X POST http://localhost:3000/bookings \\\n -H \"Content-Type: application/json\" \\\n -d '{\"passenger_name\": \"Jane Doe\"}' | jq .\n\nGot it working quite nicely.\n\n\n {\n \"statusCode\": 400,\n \"error\": \"Bad Request\",\n \"message\": \"missing required property request.body.trip_id\",\n \"errors\": [\n {\n \"path\": \".body.trip_id\",\n \"message\": \"must have required property 'trip_id'\",\n \"fullMessage\": \"missing required property request.body.trip_id\"\n }\n ]\n }\n\nAll that took was this:\n\n\n import Fastify from \"fastify\";\n import { createRequire } from \"node:module\";\n import { join, dirname } from \"node:path\";\n import { fileURLToPath } from \"node:url\";\n\n const require = createRequire(import.meta.url);\n const { OpenApiValidator } = require(\"openapi-data-validator\");\n\n const specPath = join(dirname(fileURLToPath(import.meta.url)), \"openapi.yaml\");\n const fastify = Fastify({ logger: true });\n const validate = new OpenApiValidator({ apiSpec: specPath }).createValidator();\n\n fastify.addHook(\"preHandler\", async (request, reply) => {\n try {\n await validate({\n method: request.method,\n route: request.routeOptions.url,\n headers: request.headers,\n query: request.query,\n body: request.body,\n path: request.params,\n });\n } catch (err) {\n return reply.status(err.status ?? 400).send({\n statusCode: err.status ?? 400,\n error: \"Bad Request\",\n message: err.message,\n errors: err.errors,\n });\n }\n });\n\n fastify.post(\"/bookings\", async (request, reply) => {\n return reply.status(201).send({\n id: crypto.randomUUID(),\n trip_id: request.body.trip_id,\n passenger_name: request.body.passenger_name,\n has_bicycle: request.body.has_bicycle ?? false,\n has_dog: request.body.has_dog ?? false,\n });\n });\n\n await fastify.listen({ port: 3000 });\n\n\nPerfectly vanilla Fastify setup, all conventions intact, but I can rely on OpenAPI being validated as pre-hook before my route handlers are even touched, meaning they don't need anywhere near as much manual validation inside the handler.\n\nFor me, this would do the job perfectly. If I wanted to publish docs I'd simply publish directly to Scalar with Github Action or CLI/CI, instead of awkwardly hosting it in the API itself. Same goes for SDKs, I'd trigger a new build of Speakeasy on merge to `main` and keep SDK logic out of this individual API, which could be one of many.\n\nPerhaps somebody could wrap this approach up ☝️ and create `fastify-openapi-middleware`, and remove the old middleware off the Fastify Ecosystem page as it's long dead.\n\nLet me know if you do! 🫡",
"title": "Which of Fastify's many OpenAPI plugins are the best?",
"updatedAt": "2026-05-15T13:32:50.919Z"
}