How to generate OpenAPI with Fastify
Properly investing in an API requires creating an OpenAPI document that accurately describes the API, enabling documentation, testing, security scans, governance, SDK generation, and more. OpenAPI is a powerful tool for API teams, but it can be daunting to create and maintain an OpenAPI document on top of building the actual API.
With the rise in popularity of API design-first, some APIs might have declared their OpenAPI before writing the code, but for many simple uses like quickly building a backend for a single frontend within the same team, most Fastify users have already done the hard part: defining their API routes and schemas, which can be used to generate an OpenAPI document.
Once the Fastify OpenAPI plugin has generated the OpenAPI document from those existing route definitions, it can be used for documentation and SDK generation, which is a pretty powerful pipeline. See how to do this with Speakeasy for the SDK, and Scalar for the docs, and help API consumers integrate with the API easily.
Example code
Follow this guide with the Fastify example in this repository at
examples/frameworks-fastify.
Here is what we will cover:
- Install and configure
@fastify/swagger, and don’t worry it supports OpenAPI v3.x. - Generate an OpenAPI document from route schemas.
- Add reusable component schemas with
fastify.addSchema(). - Define stable
operationIdand tags for SDK ergonomics. - Add Speakeasy extensions like
x-speakeasy-retries. - Generate SDKs from the produced OpenAPI document.
Requirements
This guide assumes there is a Fastify application serving up API endpoints, and the reader has basic familiarity with route schemas.
Install Fastify CLI and dependencies
To make sure the Fastify CLI is available for generating OpenAPI, install it globally:
npm install --global fastify-cli@^8Verify your CLI:
fastify versionIf fastify is not available on path, use npx fastify-cli in commands.
The specific versions of packages will change over time, but these are the versions being used in this guide and the sample code that goes with it.
{
"dependencies": {
"@fastify/autoload": "^6.3.0",
"@fastify/sensible": "^6.0.0",
"@fastify/swagger": "^9.7.0",
"@scalar/fastify-api-reference": "^1.25.11",
"fastify": "^5.8.0",
"fastify-cli": "^8.0",
"fastify-plugin": "^5.1.0"
},
"devDependencies": {
"standard": "^17.1.0"
}
}Not all of these are required for every setup, but the autoload plugin cuts down on boilerplate for this example. The key one for OpenAPI generation is @fastify/swagger, and @scalar/fastify-api-reference is used in this example to serve up API documentation from the generated OpenAPI document.
Install those new dependencies and lets get Fastify setup.
npm install Register Fastify “Swagger” plugin
Register it in plugins/openapi.js:
import fp from "fastify-plugin";
import swagger from "@fastify/swagger";
import scalar from "@scalar/fastify-api-reference";
export default fp(async (fastify) => {
await fastify.register(swagger, {
openapi: {
openapi: "3.1.2",
info: {
title: "Train Travel API",
description: "API for finding and booking train trips across Europe.",
contact: {
name: "Train Support",
url: "https://example.com/support",
email: "support@example.com",
},
license: {
name: "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International",
url: "https://creativecommons.org/licenses/by-nc-sa/4.0/",
},
version: "1.2.1",
},
servers: [
{
url: "https://try.microcks.io/rest/Train+Travel+API/1.0.0",
description: "Mock Server",
},
{
url: "https://api.example.com",
description: "Production",
},
],
},
});
await fastify.register(scalar, {
routePrefix: "/reference",
});
});Generate OpenAPI from your app:
fastify generate-swagger app.js > openapi.jsonWhat happens next will depend very much on how the routes are defined and how much information has been put into fastify.addSchema() already. This is one of the few frameworks around that has done a lot of the legwork already in terms of defining schemas for validation, so the generated OpenAPI document is often pretty good right out of the gate, and can be improved incrementally from there.
{
"openapi": "3.1.2",
"info": {
"title": "Train Travel API",
"description": "API for finding and booking train trips across Europe.",
"contact": {
"name": "Train Support",
"url": "https://example.com/support",
"email": "support@example.com"
},
"license": {
"name": "Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International",
"url": "https://creativecommons.org/licenses/by-nc-sa/4.0/"
},
"version": "1.2.1"
},
"components": {
"securitySchemes": {
"OAuth2": {
"type": "oauth2",
"description": "OAuth 2.0 authorization code flow.",
"flows": {
"authorizationCode": {
"authorizationUrl": "https://example.com/oauth/authorize",
"tokenUrl": "https://example.com/oauth/token",
"scopes": {
"read": "Read access",
"write": "Write access"
}
}
}
}
},
"schemas": {
"Wrapper-Collection": {
// ...
},
"Links-Self": {
// ...
},
"Links-Pagination": {
// ...
},
"Booking": {
"description": "A booking for a train trip.",
"type": "object",
"required": [
"trip_id",
"passenger_name"
],
"properties": {
"id": {
"type": "string",
"format": "uuid",
"readOnly": true
},
"trip_id": {
"type": "string",
"format": "uuid"
},
"passenger_name": {
"type": "string"
},
"has_bicycle": {
"type": "boolean"
},
"has_dog": {
"type": "boolean"
}
}
}
}
},
"paths": {
"/bookings": {
"post": {
"operationId": "create-booking",
"tags": [
"Bookings"
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"trip_id",
"passenger_name"
],
"properties": {
"trip_id": {
"type": "string",
"format": "uuid"
},
"passenger_name": {
"type": "string"
},
"has_bicycle": {
"type": "boolean",
"default": false
},
"has_dog": {
"type": "boolean",
"default": false
}
}
}
}
}
},
"security": [
{
"OAuth2": [
"write"
]
}
],
"responses": {
"201": {
"description": "A booking with a self link.",
"content": {
"application/json": {
"schema": {
"allOf": [
{ "$ref": "#/components/schemas/Booking" },
{ "properties": { "links": { "$ref": "#/components/schemas/Links-Self" } } }
]
}
}
}
}
}
}
}
},
"servers": [
{
"url": "https://api.example.com",
"description": "Production"
}
],
"security": [
{
"OAuth2": [
"read"
]
}
]
}This output has been chopped for brevity, but you can see the OpenAPI version, the info section from the plugin config, the reusable schemas from fastify.addSchema(), and the paths generated from route definitions with request body and response schemas.
Where are those schemas coming from? Two places:
- Generally reusable components
- Route-specific schemas defined inline in route definitions
Add Reusable Component Schemas
Use fastify.addSchema() untethered from any actual routes, to define reusable schemas in one place.
Here various components are being defined like a collection wrapper to standardize JSON collection responses, and some handy links that can be cherry-picked into various resources. Then domain-specific schemas like Station, Trip, and Booking are defined, which can be referenced from route definitions with $ref: "SchemaName". These are defined once so they can be reused across request and response for GET/POST/PATCH routes.
import fp from "fastify-plugin";
export default fp(async (fastify) => {
fastify.addSchema({
$id: "Wrapper-Collection",
type: "object",
properties: {
data: { type: "array", items: { type: "object" } },
links: { type: "object", readOnly: true },
},
});
fastify.addSchema({
$id: "Links-Self",
type: "object",
properties: { self: { type: "string", format: "uri" } },
});
fastify.addSchema({
$id: "Links-Pagination",
type: "object",
properties: {
next: { type: "string", format: "uri" },
prev: { type: "string", format: "uri" },
},
});
fastify.addSchema({
$id: "Station",
type: "object",
required: ["id", "name", "address", "country_code"],
properties: {
id: { type: "string", format: "uuid" },
name: { type: "string" },
address: { type: "string" },
country_code: { type: "string" },
timezone: { type: "string" },
},
});
fastify.addSchema({
$id: "Trip",
type: "object",
properties: {
id: { type: "string", format: "uuid" },
origin: { type: "string", format: "uuid" },
destination: { type: "string", format: "uuid" },
departure_time: { type: "string", format: "date-time" },
arrival_time: { type: "string", format: "date-time" },
price: { type: "number" },
operator: { type: "string" },
bicycles_allowed: { type: "boolean" },
dogs_allowed: { type: "boolean" },
},
});
fastify.addSchema({
$id: "Booking",
type: "object",
required: ["trip_id", "passenger_name"],
properties: {
id: { type: "string", format: "uuid", readOnly: true },
trip_id: { type: "string", format: "uuid" },
passenger_name: { type: "string" },
has_bicycle: { type: "boolean" },
has_dog: { type: "boolean" },
},
});
});Then reference those schemas from route responses:
fastify.post(
"/bookings",
{
schema: {
operationId: "create-booking",
tags: ["Bookings"],
security: [{ OAuth2: ["write"] }],
body: {
type: "object",
required: ["trip_id", "passenger_name"],
properties: {
trip_id: { type: "string", format: "uuid" },
passenger_name: { type: "string" },
has_bicycle: { type: "boolean", default: false },
has_dog: { type: "boolean", default: false },
},
},
response: {
201: {
allOf: [
{ $ref: "Booking" },
{ properties: { links: { $ref: "Links-Self" } } },
],
},
},
},
},
async function (request, reply) {
// ... implementation
}The body schema is emitted directly into the generated OpenAPI document as the requestBody for that operation. Any request that doesn’t match — wrong type, missing required field, bad format — is rejected with a 400 before the handler is invoked.
For example, sending a non-UUID trip_id:
curl -s -X POST http://localhost:3000/bookings \
-H "Content-Type: application/json" \
-d '{"trip_id": 123, "passenger_name": "John Doe"}' | jq .Fastify automatically returns the following error.
{
"statusCode": 400,
"code": "FST_ERR_VALIDATION",
"error": "Bad Request",
"message": "body/trip_id must match format \"uuid\""
}The error path (body/trip_id) and the failing keyword (format) are both surfaced automatically from AJV, with no extra error-handling code required in the route.
Customizing operationId using Fastify
With the OpenAPI now improving, its time to make the SDK generation more predictable.
Each path’s operationId field in the OpenAPI document is used to generate method names and documentation in SDKs.
To add operationId to a route, add the field to the route’s schema. For example:
fastify.get("/stations", {
schema: {
operationId: "getStations",
tags: ["Stations"],
querystring: {
type: "object",
properties: {
country_code: { type: "string", description: "Filter by country code" }
}
},
},
});Add OpenAPI tags to Fastify routes
At Speakeasy, whether building a big application or only having a handful of operations, it’s a good idea to add tags to all Fastify routes to group them by tag in generated SDK code and documentation.
To add tags to a route, include the tags array in the route’s schema:
fastify.get('/stations', {
schema: {
operationId: 'get-stations',
tags: ['Stations'],
// ...
},
}, async function (request, reply) { /* ... */ });Add metadata to tags
Tag descriptions can be added in plugins/openapi.js via the top-level tags array in the openapi config:
openapi: {
// ...
tags: [
{
name: "Stations",
description: "Find and filter train stations across Europe.",
},
{
name: "Trips",
description: "Timetables and routes for train trips between stations.",
},
{
name: "Bookings",
description: "Create and manage bookings for train trips.",
},
{
name: "Payments",
description: "Pay for bookings and view payment status.",
},
],
}Fastify copies the tags list into the generated OpenAPI document verbatim. Any tag used on a route but not listed here will still appear in the output, but without a description.
Add Security and Speakeasy Retries
When using Speakeasy to generate an SDK, customize it to follow custom rules for retrying failed requests. For instance, if the server fails to return a response within a specified time, the SDK can retry the request without clobbering the server.
Add retries to SDKs generated by Speakeasy by adding a top-level x-speakeasy-retries schema to the OpenAPI spec. Override the retry strategy per operation by adding x-speakeasy-retries to specific endpoints.
openapi: {
security: [{ OAuth2: ["read"] }],
components: {
securitySchemes: {
OAuth2: {
type: "oauth2",
flows: {
authorizationCode: {
authorizationUrl: "https://example.com/oauth/authorize",
tokenUrl: "https://example.com/oauth/token",
scopes: {
read: "Read access",
write: "Write access",
},
},
},
},
},
},
"x-speakeasy-retries": {
strategy: "backoff",
backoff: {
initialInterval: 500,
maxInterval: 60000,
maxElapsedTime: 3600000,
exponent: 1.5,
},
statusCodes: ["5XX"],
retryConnectionErrors: true,
},
}How To Generate an SDK Based on OpenAPI Spec
Before generating an SDK, save the Fastify-generated OpenAPI spec to a file. Add the following script to package.json to generate openapi.json in the root of the project:
{
"scripts": {
"openapi": "fastify generate-swagger app.js > openapi.json"
}
}Then run the following in the terminal:
npm run openapiAfter following the steps above, the OpenAPI spec is ready to use as the basis for a new SDK. Use Speakeasy to generate an SDK.
In the root directory of the project, run the following:
speakeasy quickstartFollow the onscreen prompts to provide the necessary configuration details for your new SDK, such as the name, schema location, and output path. Enter openapi.json when prompted for the OpenAPI document location and select TypeScript when prompted for which language you would like to generate.
Add SDK Generation to GitHub Actions
The Speakeasy sdk-generation-action repository provides workflows that can integrate the Speakeasy CLI in a CI/CD pipeline, so SDKs are regenerated when the OpenAPI spec changes.
Set up Speakeasy to automatically push a new branch to SDK repositories so that engineers can review and merge the SDK changes.
For an overview of how to set up automation for your SDKs, see the Speakeasy documentation about SDK Generation Action and Workflows.
Summary
This tutorial covered how to generate an OpenAPI specification for a Fastify API and how to integrate Fastify with Speakeasy to generate SDKs. The tutorial provided step-by-step instructions, from adding @fastify/swagger to a Fastify project and generating an OpenAPI specification, to improving the generated OpenAPI specification for better SDK generation.
It also covered how to use the Speakeasy OpenAPI extensions to improve generated SDKs and how to automate SDK generation as part of a CI/CD pipeline.
Following these steps will successfully generate OpenAPI specifications for Fastify applications and improve API operations.
Last updated on