# How to generate OpenAPI with Fastify

# 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.

<Callout title="Example code" type="info">
  Follow this guide with the Fastify example in this repository at
  <code>examples/frameworks-fastify</code>.
</Callout>

Here is what we will cover:

1. Install and configure `@fastify/swagger`, and don't worry it supports OpenAPI v3.x.
2. Generate an OpenAPI document from route schemas.
3. Add reusable component schemas with `fastify.addSchema()`.
4. Define stable `operationId` and tags for SDK ergonomics.
5. Add Speakeasy extensions like `x-speakeasy-retries`.
6. 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:

```bash
npm install --global fastify-cli@^8
```

Verify your CLI:

```bash
fastify version
```

If `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.

```json filename="examples/frameworks-fastify/package.json"
{
  "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.

```bash
npm install 
```

## Register Fastify "Swagger" plugin

Register it in `plugins/openapi.js`:

```javascript filename="plugins/openapi.js"

  await fastify.register(scalar, {
    routePrefix: "/reference",
  });
});
```

Generate OpenAPI from your app:

```bash
fastify generate-swagger app.js > openapi.json
```

What 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.

```json
{
  "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:

1. Generally reusable components
2. 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.

```javascript filename="models/schemas.js"

  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:

```javascript filename="routes/stations.js"
  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`:

```bash
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.

```json
{
  "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:

```javascript filename="routes/stations.js"
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:

```javascript filename="routes/stations.js"
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:

```javascript filename="plugins/openapi.js"
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.

```javascript filename="plugins/openapi.js"
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:

```json filename="package.json"
{
  "scripts": {
    "openapi": "fastify generate-swagger app.js > openapi.json"
  }
}
```

Then run the following in the terminal:

```bash
npm run openapi
```

After 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:

```bash
speakeasy quickstart
```

Follow 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](https://github.com/speakeasy-api/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](/docs/speakeasy-reference/workflow-file).

## 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.
