DEV Community

Thy Pham
Thy Pham

Posted on

One JSON Schema rules them all: Typescript type, API validation, OpenAPI doc, and Swagger UI.

Problem

Let's say we have an API endpoint to create a new user. The request body includes information about the user's name, age, and optional address.

This endpoint must have a request/response validator and OpenAPI documentation. It must be shown on the Swagger page as well.

To achieve this goal, we will have to create a user type, a user validation schema for validating the request and response, and another user schema for the OpenAPI doc and Swagger page.

// Typescript type
type User = {
  address?: string | undefined;
  name: string;
  age: number;
};
Enter fullscreen mode Exit fullscreen mode
// Validation schema
app.post(
  '/users',
  body('address').isString(),
  body('age').isNumber().notEmpty(),
  body('name').isString().notEmpty(),
  (req: Request, res: Response) => {
    // ...
  },
);
Enter fullscreen mode Exit fullscreen mode
---
openapi: 3.0.0
info:
  title: Sample API Spec
  version: 1.0.0
servers:
- url: http://localhost
paths:
  "/users":
    post:
      summary: Create new user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              additionalProperties: false
              type: object
              properties:
                name:
                  type: string
                age:
                  type: number
                address:
                  type: string
              required:
              - name
              - age
      responses:
        '200':
          description: successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                required:
                - message
        '500':
          description: error response
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  error:
                    type: string
                required:
                - message
                - error
Enter fullscreen mode Exit fullscreen mode

Defining three schemas for the user is code redundancy. The problem will come when we have to, for example, add a new field named job in the request body. Then we will have to modify all three places in our code for that update.

Solution

There is a way that allows us to create just one schema and use it for static type, API Validation, OpenAPI doc, and Swagger page. The answer is JSON Schema, with the help from these libraries:

As you might have already known, OpenAPI uses JSON Schema to define its data types. So the last missing piece of our solution is:

  • @sinclair/typebox: this lib helps us define in-memory JSON Schema and use it as Typescript type.

So the main idea is to use Typebox to create a user JSON Schema. Then use this schema in the OpenAPI specification. Finally, use the OpenAPI spec in API validation and build the Swagger page.

Create user JSON schema

import { Static, Type } from '@sinclair/typebox';

/**
 * The Schema below is the same as
 * {
 *   additionalProperties: false,
 *   type: 'object',
 *   properties: {
 *     name: { type: 'string' },
 *     age: { type: 'number' },
 *     address: { type: 'string' }
 *   },
 *   required: [ 'name', 'age' ]
 * }
 */
const UserSchema = Type.Strict(
  Type.Object(
    {
      name: Type.String(),
      age: Type.Number(),
      address: Type.Optional(Type.String()),
    },
    { additionalProperties: false },
  ),
);

/**
 * The type below is the same as
 * type User = {
 *     address?: string | undefined;
 *     name: string;
 *     age: number;
 * }
 */
type User = Static<typeof UserSchema>;

export { User, UserSchema };
Enter fullscreen mode Exit fullscreen mode

Use user JSON schema to create OpenAPI specification

import { OpenAPIV3 } from 'express-openapi-validator/dist/framework/types';
import { ErrorResponseSchema } from './ErrorResponse';
import { SuccessResponseSchema } from './SuccessResponse';
import { UserSchema } from './User';

const apiSpec: OpenAPIV3.Document = {
  openapi: '3.0.0',
  info: {
    title: 'Sample API Spec',
    version: '1.0.0',
  },
  servers: [
    {
      url: 'http://localhost',
    },
  ],
  paths: {
    '/users': {
      post: {
        summary: 'Create new user',
        requestBody: {
          required: true,
          content: {
            'application/json': {
              schema: UserSchema as OpenAPIV3.SchemaObject,
            },
          },
        },
        responses: {
          200: {
            description: 'successful response',
            content: {
              'application/json': {
                schema: SuccessResponseSchema as OpenAPIV3.SchemaObject,
              },
            },
          },
          500: {
            description: 'error response',
            content: {
              'application/json': {
                schema: ErrorResponseSchema as OpenAPIV3.SchemaObject,
              },
            },
          },
        },
      },
    },
  },
};

export { apiSpec };
Enter fullscreen mode Exit fullscreen mode

Use the api spec above for validating the api request/response and build Swagger page

import express from 'express';
import * as OpenApiValidator from 'express-openapi-validator';
import * as swaggerUi from 'swagger-ui-express';
import { apiSpec } from './api';

const app = express();
app.use(express.json());
app.use(express.urlencoded());
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiSpec));
app.use(
  OpenApiValidator.middleware({
    apiSpec,
    validateRequests: true,
    validateResponses: true,
  }),
);
app.post('/users', (req, res) => {
  res.json({
    message: 'successful',
  });
});
app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

As you can see in the code above, we only have to define the user schema once using Typebox. Whenever we need to update the user schema, we only have to change the code in one place. The API validation and the OpenAPI doc, Swagger page will be updated accordingly.

Discussion (0)