Do Not Repeat Yourself With Swagger, Joi and Typescript Interfaces

Leonid Ponomarev
The Startup
Published in
5 min readJan 17, 2021

Our environment:

  • Express Server
  • TypeScript
  • Joi validation
  • Swagger

Our task: DRY

For this task we should create Swagger documentation and TypeScript Interfaces just from Joi-Schemas!

Ready? Go!

Step 1: Installing Dependencies

npm init
npm install --save express http body-parser swagger-ui-express joi joi-to-swagger @babel/core @types/node typescript ts-node
npm install --save-dev gulp @babel/register @babel/plugin-proposal-class-properties @babel/preset-env @babel/preset-typescript json-schema-to-typescript

You may need to install ts-node globally if local version will not run:

npm install -g ts-node

The first group of tools allows us to make a swag, the second will create TypeScript interfaces for us.

Step 2: Creating the Express Server

First things first: we need to serve routers. For our example we will create syntetic login-service with one “/login” route and two possible queryes:

  • / GET to see user’s list
  • / POST to create the new user

Out future project structure going to be:

package.json
index.ts
routes/
login/
index.ts
login-get.route.ts
login-post.route.ts
login.spec/
login.schema.ts
gulpfile.ts
swagger.def.ts

Ok. First add the starting script into package.json:

"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "ts-node index.ts"
},

And create the index.ts file:

const express = require('express'),
http = require('http')
const app = express()
const bodyParser = require('body-parser').json()
app.use(bodyParser)
const server = http.createServer(app)
const hostname = '0.0.0.0'
const port = 3001
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`)
})

This code creates an empty server at localhost:3001/

Now you can start it by the command

npm start

Run your brouser, go to the http://localhost:3001/

You will see “Cannot GET /” message! Server works!

Step 3. Adding the SWAGGER

Create swagger.def.ts file with base openAPI specs:

const swagger = {
openapi: '3.0.0',
info: {
title: 'Express API',
version: '1.0.0',
description: 'The REST API test service'
},
servers: [
{
url: 'http://localhost:3001',
description: 'Development server'
}
],
paths: {
}
}
export default swagger

Then modify index.ts file to create Swagger and include our specs:

const express = require('express'),
http = require('http'),
swaggerUI = require('swagger-ui-express')
import swDocument from './swagger.def'
const app = express()
const bodyParser = require('body-parser').json()
app.use(bodyParser)
app.use('/api-docs',swaggerUI.serve,swaggerUI.setup(swDocument))const server = http.createServer(app)
const hostname = '0.0.0.0'
const port = 3001
server.listen(port, hostname, () => {
console.log(`Server running at http://${hostname}:${port}/`)
})

Now the time to test the swagger. Run your project and go to the ‘http://localhost:3001/api-docs/’. You should see the picture:

Step 4. Adding the Router

Let’s create the routes! Add next lines after app.use(‘/api-docs’,….) into your index.ts file:

import loginRouter from './routes/login'
app.use('/login', loginRouter)

Create the folder path routes/login/ with the file index.ts inside:

const express = require('express')// here the our swagger info
export const swLoginRouter = {
"/login": {
"get": {
},
"post": {
}
}
}
// here the routes
const router = express()
router.get('/', function (req, res) {
res.send('This is a login GET service')
})
router.post('/', function (req, res) {
res.send('This is a login POST service')
})
export default router

Modify the swagger.def.ts to include our path:

import {swLoginRouter} from './routes/login/'
....
paths: {
...swLoginRouter
}

Start the server. You can see the working answers at

Step 5. Adding the Routes

Create the login-get.route.ts inside routes/login/ folder:

// swagger info
export const swGetUser = {
"summary": "Retrieve the list with all of the users",
"tags": [
"login"
],
"responses": {
"200": {
"description": "Object with users info"
}
}
}
// the route
export default async (req, res) => {
res.send('This is a login GET service')
}

You can see the swagger information in the code and a very simple route.

Create the login-post.route.ts:

export const swPostUser = {
"summary": "Create the new user",
"tags": [
"login"
],
"requestBody": {
"content": {
"application/json": {
"schema": {
}
}
}
},
"responses": {
"200": {
"description": "User created"
},
"default": {
"description": "Error message"
}
}
}
export default async (req, res) => {
res.send('This is a login POST service')
}

In contrast to ‘GET’ query, the ‘POST’ have requestBody information, because we want to get it from the clients.

Modify the routes/login/index.ts to include all what we have created:

import getUsersList, {swGetUser} from './login-get.route'
import createTheUser, {swPostUser} from './login-post.route'
const express = require('express')export const swLoginRouter = {
"/login": {
"get": {
...swGetUser
},
"post": {
...swPostUser
}
}
}
const router = express()
router.get('/', function (req, res) {
getUsersList(req, res)
})
router.post('/', function (req, res) {
createTheUser(req, res)
})
export default router

You cat run your server again ang see much more detailed swagger’s description.

Step 6: Creating the Schema

We will need the Joi-Schema for ‘POST’ queries only. Lets create a file login.schema.ts inside login.spec/ folder:

const joi = require('joi')
export const joiSchema = joi.object().keys({
mode: joi.string().required(),
email: joi.string().email()
})
const j2s = require('joi-to-swagger')
const schema = j2s(joiSchema).swagger
export default schema

This shema requires the ‘mode’ info and optional ‘email’ info from the request body.

Include this schema into login-post.route.ts:

import schema, {joiSchema} from './login.spec/login.schema'
...
"schema": {
...schema
}

Restart the server. Go to ‘localhost:3001/api-docs’ in your brouser and see the schema in your swagger! Congratulations. Now we done with self-documentation!

Step 7. Data Validation

Modify the login-post.route.ts file to add the Joi validation:

export default async (req, res) => {
try {
await joiSchema.validateAsync(req.body)
res.send('This is a login POST service')
} catch(err) {
res.send(err)
}
}

Try to send some wrong data via swagger interface and see the results!

Step 8. Automatic TypeScript Interface Creation

We will do this via Gulp Task. Create the gulpfile.ts inside your project root:

const gulp = require('gulp')
const through = require('through2')
import { compile } from 'json-schema-to-typescript'
const fs = require('fs')
const endName = "schema.ts"
const routes = `./routes/**/*.spec/*${endName}`
function path(str: string) : string
{
let base = str
if(base.lastIndexOf(endName) != -1)
base = base.substring(0, base.lastIndexOf(endName))
return base
}
gulp.task('schema', () => {
return gulp.src(routes)
.pipe(through.obj((chunk, enc, cb) => {
const filename = chunk.path
import(filename).then(schema => { // dynamic import
console.log('Converting', filename)
compile(schema.default, `IDTO`)
.then(ts => {
//console.log(path(filename).concat('d.ts'), ts)
fs.writeFileSync(path(filename).concat('d.ts'), ts)
})
})
cb(null, chunk)
}))
})
// watch service
const { watch, series } = require('gulp')
exports.default = function() {
watch(routes, series('schema'))
}

This script will seatch all files with ‘*schema.ts’ names inside ‘routes/**/*.spec’ catalogues. You can rewrite the script as you needs.

Run gulp task:

gulp

Resave your schema. You will recieve the log:

[12:30:32] Starting 'schema'...
[12:30:32] Finished 'schema' after 62 ms
Converting .../routes/login/login.spec/login.schema.ts

Inside login.spec/ you will find your TypeScript Interface!

--

--