One JSON Schema rules them all: Typescript type, API validation, OpenAPI doc, and Swagger UI.
Thy Pham
Posted on January 30, 2022
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;
};
// Validation schema
app.post(
'/users',
body('address').isString(),
body('age').isNumber().notEmpty(),
body('name').isString().notEmpty(),
(req: Request, res: Response) => {
// ...
},
);
---
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
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:
express-openapi-validator: Validating API request and response using OpenAPI specification.
swagger-ui-express: Generate Swagger page using OpenAPI specification.
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 };
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 };
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);
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.
Posted on January 30, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
January 30, 2022