Building a self-documenting server in Node.JS

Conditions:





  • validation via Joi





  • using Typescript





  • Express server





  • SWAGGER at / api-docs





Task: DRY





Decision:

First you need to decide what comes first: the Joi scheme, Swagger or the TypeScript interface. It has been empirically established that Joi should be made primary.





1. Installing all modules on Express

npm install --save swagger-ui-express
      
      



Add lines to app.ts (index.ts):





import swaggerUI = require('swagger-ui-express')
import swDocument from './openapi'
...
app.use('/api-docs',swaggerUI.serve,swaggerUI.setup(swDocument))

      
      



2. Create ./openapi.ts

This file contains basic information about the server. You can create it (like all the schemes below) using the SWAGGER utility. It is important to choose the openapi v3.0.0 protocol





:





import {swLoginRoute} from './routes/login'

const swagger = {
  openapi: '3.0.0',
  info: {
    title: 'Express API for Dangle',
    version: '1.0.0',
    description: 'The REST API for Dangle Panel service'
  },
  servers: [
    {
      url: 'http://localhost:3001',
      description: 'Development server'
    }
  ],
  paths: {
    ...swLoginRoute
  },
}

export default swagger
      
      



.





3.

openapi-





./routes/login/index.ts:





import {swGetUserInfo} from './get-user-info'
import {swUpdateInfo} from './update-info'
export const swLoginRoute = {
  "/login": {
    "get": {
      ...swGetUserInfo
    },
    "patch": {
      ...swUpdateInfo
    }
  }
}
      
      



/login, : get patch. get-user-into.ts update-info.ts. .





4.

, Joi-.





.





: , .





update-info.ts, ( ):





import schema, {joiSchema} from './update-info.spec/schema'

export const swUpdateInfo = {
  "summary": "update the user info",
  "tags": [
    "login"
  ],
  "parameters": [
    {
      "name": "key",
      "in": "header",
      "schema": {
        "type": "string"
      },
      "required": true
    }
  ],
  "requestBody": {
    "content": {
      "application/json": {
        "schema": {
          ...schema
        }
      }
    }
  },
  "responses": {
    "200": {
      "description": "Done"
    },
    "default": {
      "description": "Error message"
    }
  }
}
// ...   

      
      



JSON- Swagger-, . :





"schema": {
  ...schema
}

      
      



.





Joi- :





await joiSchema.validateAsync(req.body)
      
      



4. Joi-

Joi:





npm install --save joi joi-to-swagger
      
      



:





const joi = require('joi')
const j2s = require('joi-to-swagger')

// Joi
export const joiSchema = joi.object().keys({
  mode:    joi.string().required(),
  email:   joi.string().email()
})
// end of Joi

const schema = j2s(joiSchema).swagger
export default schema

      
      



Joi- swagger-.





, SWAGGER- . TypeScript-





5. TypeScript

npm install --save-dev gulp @babel/register @babel/plugin-proposal-class-properties @babel/preset-env @babel/preset-typescript
      
      



Gulp. , . gulpfile.ts :





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('interface.ts'), ts)
            fs.writeFileSync(path(filename).concat('interface.ts'), ts)
          })
        })
      cb(null, chunk)
    }))
})


// watch service
const { watch, series } = require('gulp')
exports.default = function() {
  watch(routes, series('schema'))
}
      
      



The script bypasses all subdirectories named * .spec inside the directory from the router. There it looks for files with the names * schema.ts and creates a number of files with the names * interface.ts





Conclusion

Of course, these large and complex JSON objects with the openAPI specification are scary, but you need to understand that they are not written by hand, but generated by the SWAGGER utility.





Due to inexperience, the initial adjustment of the mechanism can take some time, but this will lead to savings of hundreds of hours that could be spent on routine!








All Articles