Craft OpenAPI Specs & Production-Ready SDKs with Fastify
Tags: fastify, openapi, api, typescript
Published: 2023-11-09 20:45:26

# Generate SDKs with Fastify

In this tutorial, we'll show you how to generate an OpenAPI specification using [Fastify](https://fastify.dev/) so that you can use Speakeasy to generate client SDKs for your API.

Here's what we'll cover:

1.  How to add `@fastify/swagger` to a Fastify project.
2.  Generate an OpenAPI specification using the Fastify CLI.
3.  Improve the generated OpenAPI specification for better downstream SDK generation.
4.  Using the Speakeasy CLI to generate a client SDK based on our generated OpenAPI specification.
5.  Use the Speakeasy OpenAPI extensions to improve generated SDKs.
6.  How to automate this process as part of a CI/CD pipeline.

Your Fastify project might not be as simple as our example app, but the steps below should translate well to any Fastify project. We'll also look at how to gradually add routes to OpenAPI so that you have the option to ship an SDK that improves API coverage over time.

<Callout title="TIP" variant="success">If you want to follow along, you can use the [**Fastify Speakeasy Bar example repository**](https://github.com/speakeasy-api/speakeasy-fastify-example).</Callout>

## The SDK Generation Pipeline

With Speakeasy, you can create client SDKs based on an OpenAPI specification. Fastify ships with the [`@fastify/swagger`](https://github.com/fastify/fastify-swagger) plugin, which provides convenient shortcuts for generating good OpenAPI specifications. We'll start this tutorial by registering `@fastify/swagger` in a Fastify project to generate a spec.

The quality of your OpenAPI specification will ultimately determine the quality of generated SDKs and documentation, so we'll dive into ways you can improve the generated specification.

With our new and improved OpenAPI specification in hand, we'll take a look at how to generate client SDKs using Speakeasy.

Finally, we'll add this process to a CI/CD pipeline so that Speakeasy automatically generates fresh SDKs whenever your Fastify API changes in the future.

## Requirements

This guide assumes that you have an existing Fastify app, or that you'll clone our example application, and that you have a basic familiarity with Fastify.

You'll need [Node.js installed](https://nodejs.org/en/download) (we used Node v20.5.1), and you'll need to install the [Fastify CLI](https://github.com/fastify/fastify-cli/).

Once you have Node.js, you can install the Fastify CLI by running the following in the terminal:

```bash
npm install fastify-cli --global
```

Make sure `fastify` is in your `$PATH`:

```bash
fastify version
```

If you can't run `fastify` using the steps above, you can use `npx` to run `fastify-cli` by replacing `fastify` with `fastify-cli` in our code samples.

For example:

```bash
# fastify version
npx fastify-cli version
```

Install the [Speakeasy CLI](/docs/speakeasy-cli/getting-started) to generate the SDK once you have generated your OpenAPI spec.

## How To Add "@fastify/swagger" to a Fastify Project

In your Fastify project folder, run the following in the terminal to install `@fastify/swagger`:

```bash
npm install --save @fastify/swagger
```

To register `@fastify/swagger` in our Fastify app, we added a new plugin. Here's the simplified plugin we added as `plugins/openapi.js`:

```javascript
import fp from "fastify-plugin";
import swagger from "@fastify/swagger";

export default fp(async (fastify) => {
  fastify.register(swagger);
});
```

Without any further configuration, you can generate an OpenAPI specification by running the Fastify CLI:

```bash
fastify generate-swagger app.js
```

This should print a basic OpenAPI spec in JSON format.

<Callout title="TIP" variant="success">
If you find YAML more readable than JSON, you can add `--yaml=true` to your `fastify` commands:

```bash
fastify generate-swagger --yaml=true app.js
```

The option to output YAML is [brand new](https://github.com/fastify/fastify-cli/pull/662) and, while merged, hasn't made it to a release when we wrote this tutorial.
</Callout>

## Supported OpenAPI Versions in Fastify and Speakeasy

Fastify can generate OpenAPI specifications in [OpenAPI version 2.0](https://swagger.io/specification/v2/) (formerly known as _Swagger_) or [OpenAPI version 3.0.3](https://swagger.io/specification/v3/).

Speakeasy [supports OpenAPI 3.x](/docs/openapi/validation).

We need to configure Fastify to ensure we output an OpenAPI spec that conforms to OpenAPI 3.0.3.

### How To Generate a Specification in OpenAPI Version 3.0.3 Using Fastify

In Fastify, the version of the generated OpenAPI specification is determined by the Fastify options object. To use OpenAPI 3.0.3, the options object should contain an object with the key `openapi` at its root.

Continuing our example above, we'll add an options object when we register `@fastify/swagger` in `plugins/openapi.js`:

```javascript
import fp from "fastify-plugin";
import swagger from "@fastify/swagger";

export default fp(async (fastify) => {
  fastify.register(swagger, {
    openapi: {},
  });
});
```

To verify that we now have an OpenAPI 3.0.3 spec, run:

```bash
fastify generate-swagger app.js
```

The output should start with the following JSON:

```json
{
  "openapi": "3.0.3"
  //...
}
```

## How To Add OpenAPI "info" in Fastify

Without customization, `@fastify/swagger` generates the following `info` object for our API:

```json
{
  //...
  "info": {
    "version": "8.10.1",
    "title": "@fastify/swagger"
  }
}
```

We can customize this object by updating our options object in `plugins/openapi.js`:

```javascript
import fp from "fastify-plugin";
import swagger from "@fastify/swagger";

export default fp(async (fastify) => {
  fastify.register(swagger, {
    openapi: {
      info: {
        title: "Speakeasy Bar API",
        description: "This is a sample API for Speakeasy Bar.",
        termsOfService: "http://example.com/terms/",
        contact: {
          name: "Speakeasy Bar Support",
          url: "http://www.example.com/support",
          email: "support@example.com",
        },
        license: {
          name: "Apache 2.0",
          url: "https://www.apache.org/licenses/LICENSE-2.0.html",
        },
        version: "1.0.1",
      },
    },
  });
});
```

Fastify copies this `info` object verbatim, which results in the following `info` object in our JSON:

```json
{
  "info": {
    "title": "Speakeasy Bar API",
    "description": "This is a sample API for Speakeasy Bar.",
    "termsOfService": "http://example.com/terms/",
    "contact": {
      "name": "Speakeasy Bar Support",
      "url": "http://www.example.com/support",
      "email": "support@example.com"
    },
    "license": {
      "name": "Apache 2.0",
      "url": "https://www.apache.org/licenses/LICENSE-2.0.html"
    },
    "version": "1.0.1"
  }
  //...
}
```

Another common pattern we've seen, included here for completeness, is to reuse information from the project's `package.json` when generating OpenAPI specs. This pattern takes [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) quite literally, and someone editing the package might not realize the downstream consequences.

To pull information from `package.json` in `plugins/openapi.js`:

```javascript
import packageJson from "../package.json";
import fp from "fastify-plugin";
import swagger from "@fastify/swagger";

export default fp(async (fastify) => {
  fastify.register(swagger, {
    openapi: {
      info: {
        title: packageJson.name,
        description: packageJson.description,
        version: packageJson.version,
        //...
      },
    },
  });
});
```

## Update Fastify To Generate OpenAPI Component Schemas

Fastify handles validation and serialization for Fastify apps based on schemas defined as JSON Schema but does not enforce separating schemas into reusable components.

Let's start with a hypothetical example in a route definition, `routes/drink/index.js`:

```javascript
export default async function (fastify, opts) {
  const schema = {
    params: {
      type: "object",
      properties: {
        drinkId: { type: "string" },
      },
    },
    response: {
      200: {
        type: "object",
        properties: {
          id: { type: "string" },
          name: { type: "string" },
          description: { type: "string" },
        },
      },
    },
  };

  fastify.get("/:drinkId/", { schema }, async function (request, reply) {
    const { drinkId } = request.params;
    return {
      id: drinkId,
      name: "Example Drink Name",
      description: "Example description",
    };
  });
}
```

The example above would generate the following OpenAPI schema for this route:

```json
{
  "paths": {
    "/drink/{drinkId}/": {
      "get": {
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "in": "path",
            "name": "drinkId",
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Default Response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "description": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
```

Note how the response schema is presented inline. If we defined the schema for another route that returns a drink object similarly, our OpenAPI spec, resulting SDK, and documentation would not present a drink as a reusable component schema.

Fastify provides methods to add and reuse schemas in an application.

As a start, let's separate the response schema and use the Fastify `addSchema` method:

```javascript
export default async function (fastify, opts) {
  fastify.addSchema({
    $id: "Drink",
    type: "object",
    properties: {
      name: { type: "string" },
      description: { type: "string" },
    },
  });

  const schema = {
    params: {
      type: "object",
      properties: {
        drinkId: { type: "string" },
      },
    },
    response: {
      200: {
        $ref: "Drink",
      },
    },
  };

  fastify.get("/:drinkId/", { schema }, async function (request, reply) {
    const { drinkId } = request.params;
    return {
      id: drinkId,
      name: "Example Drink Name",
      description: "Example description",
    };
  });
}
```

We added a field called `$id` to our drink schema, then called `fastify.addSchema()` to add this shared schema to the Fastify app. To use this shared schema, we reference it using the JSON Schema `$ref` keyword, referencing the shared schema `$id` field.

This generates the following OpenAPI schema:

```json
{
  "components": {
    "schemas": {
      "def-0": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "description": {
            "type": "string"
          }
        },
        "title": "Drink"
      }
    }
  },
  "paths": {
    "/drink/{drinkId}/": {
      "get": {
        "parameters": [
          {
            "schema": {
              "type": "string"
            },
            "in": "path",
            "name": "drinkId",
            "required": true
          }
        ],
        "responses": {
          "200": {
            "description": "Default Response",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/def-0"
                }
              }
            }
          }
        }
      }
    }
  }
}
```

Note how, instead of defining the response schema inline with the path schema, we now have a component schema `def-0`, which our path's response schema references as `#/components/schemas/def-0`.

This is already more useful, but if we were to generate an SDK or documentation based on this schema, the autogenerated name `def-0` would lead to documentation and methods for a schema component named `def-0`.

Our next task is to customize this name.

## Creating Useful OpenAPI "$ref"s in Fastify

By default, Fastify keeps track of all schemas added with `fastify.addSchema()` and numbers them. The default internal [function that builds these references](https://github.com/fastify/fastify-swagger/blob/ff0260811c0c319f4973665fd15517937e287040/lib/mode/dynamic.js#L17) looks like this:

```javascript
function buildLocalReference(json, baseUri, fragment, i) {
  if (!json.title && json.$id) {
    json.title = json.$id;
  }
  return `def-${i}`;
}
```

This function makes it clear where the `def-0` reference in our generated OpenAPI specification came from.

Fastify allows us to override the `buildLocalReference` function as part of our OpenAPI options object in `plugins/openapi.js`:

```javascript
import fp from "fastify-plugin";
import swagger from "@fastify/swagger";

export default fp(async (fastify) => {
  fastify.register(swagger, {
    openapi: {
      info: {
        title: "Speakeasy Bar API",
        description: "This is a sample API for Speakeasy Bar.",
        version: "1.0.1",
      },
    },
    refResolver: {
      buildLocalReference(json, baseUri, fragment, i) {
        return json.$id || `id-${i}`;
      },
    },
  });
});
```

By overriding `buildLocalReference` in the snippet above, we help Fastify to use the `$id` field as the component schema's reference. If we were to regenerate the OpenAPI spec now, we would see that `def-0` is replaced by `Drink`.

## Customizing OpenAPI "operationId" Using Fastify

Speakeasy uses each path's `operationId` field in the OpenAPI specification to generate method names and documentation in SDKs.

To add `operationId` to a route, add this field to the route's schema. For example:

```javascript
fastify.get(
  "/:drinkId/",
  { schema: { operationId: "getDrink" } },
  async function ({ params: { drinkId } }) {
    return {
      id: drinkId,
    };
  }
);
```

This would generate the following OpenAPI schema:

```json
{
  "/drink/{drinkId}/": {
    "get": {
      "operationId": "getDrink",
      "responses": {
        "200": {
          "description": "Default Response"
        }
      }
    }
  }
}
```

## Add OpenAPI Tags to Fastify Routes

At Speakeasy, whether you're building a big application or only have a handful of operations, we recommend adding tags to all your Fastify routes so you can group them by tag in generated SDK code and documentation.

### Add OpenAPI Tags to Routes in Fastify

To add OpenAPI tags to a route in Fastify, add the `tags` keyword with a list of tags to the route's schema. Here's a simplified example from `routes/drink/index.js`:

```javascript
fastify.get(
  "/:drinkId/",
  { schema: { tags: ["drinks"] } },
  async function ({ params: { drinkId } }) {
    return {
      id: drinkId,
    };
  }
);
```

### Add Metadata to Tags

We can add a description and external documentation link to each tag by adding a list of tag objects to the Swagger options object in `plugins/openapi.js`:

```javascript
import fp from "fastify-plugin";
import swagger from "@fastify/swagger";

export default fp(async (fastify) => {
  fastify.register(swagger, {
    openapi: {
      info: {
        tags: [
          {
            name: "drinks",
            description: "Drink-related endpoints",
            externalDocs: {
              description: "Find out more",
              url: "http://swagger.io",
            },
          },
        ],
      },
    },
  });
});
```

As with the other keys in the `info` options, Fastify copies the list of tags to the generated OpenAPI spec verbatim.

## Add a List of Servers to the Fastify OpenAPI Spec

When validating an OpenAPI spec, Speakeasy expects a list of servers at the root of the spec. We'll add this to our options object in `plugins/openapi.js`:

```javascript
import fp from "fastify-plugin";
import swagger from "@fastify/swagger";

export default fp(async (fastify) => {
  fastify.register(swagger, {
    openapi: {
      servers: [
        {
          url: "http://localhost",
          description: "Development server",
        },
      ],
    },
  });
});
```

## Add Retries to Your SDK With "x-speakeasy-retries"

Speakeasy can generate SDKs that follow custom rules for retrying failed requests. For instance, if your server fails to return a response within a specified time, you may want your users to retry their request without clobbering your server.

Add retries to SDKs generated by Speakeasy by adding a top-level `x-speakeasy-retries` schema to your OpenAPI spec. You can also override the retry strategy per operation by adding `x-speakeasy-retries`.

### Adding Global Retries

```javascript
import fp from "fastify-plugin";
import swagger from "@fastify/swagger";

export default fp(async (fastify) => {
  fastify.register(swagger, {
    openapi: {
      "x-speakeasy-retries": {
        strategy: "backoff",
        backoff: {
          initialInterval: 500,
          maxInterval: 60000,
          maxElapsedTime: 3600000,
          exponent: 1.5,
        },
        statusCodes: ["5XX"],
        retryConnectionErrors: true,
      },
    },
  });
});
```

Fastify respects OpenAPI extensions that start with `x-` and copies these to the root of the generated OpenAPI specification.

### Adding Retries Per Method

If we want to add a unique retry strategy to a single route, we can add `x-speakeasy-retries` to the route's schema:

```javascript
fastify.get(
  "/:drinkId/",
  {
    schema: {
      "x-speakeasy-retries": {
        strategy: "backoff",
        backoff: {
          initialInterval: 500,
          maxInterval: 60000,
          maxElapsedTime: 3600000,
          exponent: 1.5,
        },
        statusCodes: ["5XX"],
        retryConnectionErrors: true,
      },
    },
  },
  async function (request, reply) {
    const { drinkId } = request.params;
    return {
      id: drinkId,
      name: "Example Drink Name",
      description: "Example description",
    };
  }
);
```

Once again, when generating an OpenAPI spec, Fastify will copy route-specific OpenAPI extensions without any changes.

## How To Generate an SDK Based on Your OpenAPI Spec

Before generating an SDK, we need to save the Fastify-generated OpenAPI spec to a file. We'll add the following script to our `package.json` to generate `openapi.json` in the root of our project:

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

Then we run the following in the terminal:

```bash
npm run openapi
```

After following the steps above, we have an OpenAPI spec that is ready to use as the basis for a new SDK. Now we'll use Speakeasy to generate an SDK.

In the root directory of your project, run the following:

```bash
speakeasy generate sdk \
    --schema openapi.json \
    --lang typescript \
    --out ./sdk
```

This command uses Speakeasy to generate a new TypeScript SDK based on the OpenAPI spec Fastify generated and saved as `openapi.json`. The Speakeasy CLI saves the new SDK in the `./sdk` directory.

## Add SDK Generation to Your 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 your CI/CD pipeline, so your client SDKs are regenerated when your OpenAPI spec changes.

You can set up Speakeasy to automatically push a new branch to your SDK repositories so that your 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](https://www.speakeasyapi.dev/docs/workflow-reference).

## Summary

In this tutorial, we've learned how to integrate Fastify with Speakeasy to generate client SDKs for your Fastify API. The tutorial guides you through step-by-step instructions on how to do this, 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 covers 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, you can successfully generate OpenAPI specifications for your Fastify app and improve your API operations.