Validating swagger docs with typescript

Steven Chetwynd

javascript typescript

development

626 Words … ⏲ Reading Time: 2 Minutes, 50 Seconds

The problem #

For a while now, I’ve been using swagger to document my APIs, this provides me, and the consumers of my API to easily see what each route does, and what is required to use the route.

However it is very easy to update the code and forget to update the swagger docs, so I end up in a situation where the swagger docs are telling my API consumers the wrong thing. Not good.

The solution #

This builds on my previous post about using JsDocs to add typescript type checking to javascript projects

Express route handlers support types, for the request, response, and next. I can use these to add types to the request body, query, and params. I can also use them to add types for the response body (assuming its JSON).

app.get(
    "/",
    /**
     * @param {express.Request<GetParams, GetResponse, GetBody, GetQuery>} req
     * @param {express.Response<GetResponse>} res
     */
    (req, res) => {

And this can be abstracted out into a generic type to make it easier to use:

// In a type file
import { Request, Response, NextFunction } from "express";

export type ExpressData = {
    requestBody?: unknown;
    responseBody?: unknown;
    params?: unknown;
    query?: unknown;
}

export type ExpressHandler<T extends ExpressData> = (
    req: Request<
        T["params"],
        T["responseBody"],
        T["requestBody"],
        T["query"]
    >,
    res: Response<T["responseBody"]>,
    next: NextFunction
) => unknown

// in a route file
app.get(
    "/",
    /** @type {ExpressHandler<{
        responseBody: GetResponse;
        requestBody: GetBody;
        params: GetPath;
        query: GetQuery
     * }>}
     */
    (req, res) => {

But the route types still need to be linked to the swagger docs.

There are various swagger code gen packages which can convert swagger to typescript, however the one I’ve found to be easiest to use and produces the types I need is @hey-api/openapi-ts.

Here is my config for the package:

// @ts-check

/** @type {import("@hey-api/openapi-ts").UserConfig} */
module.exports = {
    input: "./swagger.json",
    output: {
        clean: true,
        path: "./src/generated-types"
    },
    plugins: [
        "@hey-api/typescript"
    ]
};

With an npm script which runs openapi-ts.

This will delete all files in ./src/generated-types, and then generate new files to put in there with the types from the swagger docs.

The generated types always have the same names, and are exported from a index.js file in the generated-types folder.

Now I can update my route file to import and use these types:

/** @typedef {import("./types").ExpressData} ExpressData */
/** @typedef {import("./generated-types/types.gen").GetData} GetData */
/** @typedef {import("./generated-types/types.gen").GetResponse} GetResponse */
/**
 * @template {ExpressData} T
 * @typedef {import("./types").ExpressHandler<T>} ExpressHandler
 */

app.get(
    "/",
    /** @type {ExpressHandler<{
        responseBody: GetResponse;
        requestBody: GetData["body"];
        params: GetData["path"];
        query: GetData["query"]
     * }>}
     */
    (req, res) => {

Now I can set my git pre-commit hook to regenerate my types from my swagger docs before running the type checks, if my types are not correct according to typescript the commit will fail and I’ll need to fix the types or change my swagger docs.

The limitation #

This will only enforce the properties specified by the swagger docs are present on the response, it will not care about extra properties, unless the response body is built as an object directly in res.json e.g

const data = await someFunction(someArgs);
// Typescript will not care about extra properties
res.json(data);

const data = await someFunction(someArgs);
// Typescript will warn about extra properties
res.json({
    hello: data.world,
    javascript: data.typescript
});

This may not be a problem for small response bodies, but for larger ones it may be impractical to construct the response object like above.

Additional properties in response bodies may not be a huge issue in most cases, if its not documented in our swagger documentation, systems integrating with the API shouldn’t use them. However if we need to ensure some data is not returned in the response, e.g. oAuth client secret, it will be important to include integration tests to check that.