Express + TypeScript + Mongo – Part 4 – Swagger
If you’re building a fairly large API you’re going to want to document it so the consumers know what to expect. There are several ways to do this. The most common way is to create and maintain a separate document that has to be updated on every change or release. An alternative way is to use something like Swagger.
Swagger is a set of tools that conform to the OpenAPI specification. It consists of two components: an interactive UI and a JSON file describing the API.
In this article, we’ll install a dependency, tsoa, that will allow us to add annotations to our endpoints and auto-generate this JSON file for us. It can also generate our Express routes file too which is nice. We will also install another dependency, swagger-ui-express, that will add the Swagger UI to our server for us as well.
Let’s get started by installing our new dependencies:
npm install --save tsoa swagger-ui-express
Once that’s done, we’ll need to update our tsconfig.json file so that it now looks like this:
{ "compilerOptions": { "target": "esnext", "moduleResolution": "node", "experimentalDecorators": true } }
This will allow us to pull tsoa into our project and use annotations, or decorators as they are called in TypeScript.
We also need to create tsoa.json:
{ "swagger": { "outputDirectory": ".", "entryFile": "./src/app.ts" }, "routes": { "basePath": "/", "entryFile": "./src/app.ts", "routesDir": "./src" } }
There are two things we’ll use tsoa for. The first is to generate our swagger JSON file, and the second is to generate our routes configuration for express.
We’re going to gut our todo.controller.ts file and replace it with this:
import { TodoModel} from './todo'; import { Route, Get, Controller, Post, BodyProp, Put, Delete, SuccessResponse } from 'tsoa'; @Route('/todo') export class TodoController extends Controller { @Get('/') public async getAll() : Promise<any[]> { try { let items: any[] = await TodoModel.find({}); items = items.map((item) => { return {id: item._id, description: item.description}}); return items; } catch (err) { this.setStatus(500); console.error('Caught error', err); } } @Post('/') public async create(@BodyProp('description') description: string): Promise<void> { const item = new TodoModel({description: description}); await item.save(); } @Put('/{id}') public async update(id: string, @BodyProp('description') description: string): Promise<void> { await TodoModel.findOneAndUpdate({_id: id}, {description: description}); } @Delete('/{id}') public async remove(id: string): Promise<void> { await TodoModel.findByIdAndRemove(id); } }
With tsoa, you create classes that contain one function for each of your API endpoints. The annotations we use tell tsoa how we want our API to work. With this information, it can then generate our routes file and swagger.json file for us.
Now, let’s go to our app.ts file and change it to this:
import * as express from 'express'; import * as cors from 'cors'; import * as bodyparser from 'body-parser'; import { requestLoggerMiddleware } from './request.logger.middleware'; import * as swaggerUi from 'swagger-ui-express'; import './todo.controller'; import { RegisterRoutes } from './routes'; const app = express(); app.use(cors()); app.use(bodyparser.json()); app.use(requestLoggerMiddleware); RegisterRoutes(app); try { const swaggerDocument = require('../swagger.json'); app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument)); } catch (err) { console.log('Unable to load swagger.json', err); } export { app };
For the most part, it’s what we already had with several notable changes. We still need to import our controller so tsoa can pick up on it. We also need to pull in a new file, routes, that exports a RegisterRoutes function. This is generated by tsoa any time we tell it generate the routes file. You can commit this to revision control if you want or you can make the route generation part of your build. For now we’re just going to commit it.
The other change that was made is that we are now importing swagger-ui-express and adding a route — /docs — for it. We are also pulling in a “swagger.json” file which will be found at the root of the project and generated by tsoa.
The last thing we’ll need to do is add a new script to package.json. Your scripts section should look like this:
"scripts": { "start": "nodemon -x ts-node src/main.ts", "generate": "tsoa swagger && tsoa routes" },
If you run
npm run generate
tsoa will start and generate the src/routes.ts and the ./swagger.json files.
You should now be able to start your server
npm run start
and interact with the API just as before. You can also view your API documentation by opening http://localhost:8080/docs.
This article just barely scratched the surface of tsoa and Swagger, but it gets us started to explore it further in the future. I’d encourage you to play around and ask questions if you get stuck or aren’t sure about something.
Source code is available here:
Hi
Thanks for your article.
When I clone your github project it is showing
import express from ‘express’;
^^^^^^^
SyntaxError: Unexpected identifier
Please can you help me how to resolve the issue.
I have tried in all scenarios. But did not work.
Thanks
Bhavani
This is always a fun error to diagnose because it’s vague and can be caused by a few things.
Let’s start with making sure you’re using Node 10.x or higher. For instance, I’m using Node 10.14.2, and I am not seeing this error. I also want to confirm that you did run ‘npm install’ after cloning.
hi,
What is the reason for this error?
$ tsoa routes && tsoa swagger
Generate routes error.
Error: Missing entryFile: Configuration must contain an entry point file.
at C:\Users\ahmet\Desktop\Project\todo-server\node_modules\tsoa\dist\cli.js:223:27
at step (C:\Users\ahmet\Desktop\Project\todo-server\node_modules\tsoa\dist\cli.js:45:23)
at Object.next (C:\Users\ahmet\Desktop\Project\todo-server\node_modules\tsoa\dist\cli.js:26:53)
at C:\Users\ahmet\Desktop\Project\todo-server\node_modules\tsoa\dist\cli.js:20:71
at new Promise ()
at __awaiter (C:\Users\ahmet\Desktop\Project\todo-server\node_modules\tsoa\dist\cli.js:16:12)
at validateRoutesConfig (C:\Users\ahmet\Desktop\Project\todo-server\node_modules\tsoa\dist\cli.js:217:55)
at Object. (C:\Users\ahmet\Desktop\Project\todo-server\node_modules\tsoa\dist\cli.js:394:42)
at step (C:\Users\ahmet\Desktop\Project\todo-server\node_modules\tsoa\dist\cli.js:45:23)
at Object.next (C:\Users\ahmet\Desktop\Project\todo-server\node_modules\tsoa\dist\cli.js:26:53)
error Command failed with exit code 1.
Likely, this is a result of a new tsoa. The tsoa.json file covered in the project is based on an older version (2.3.8). You’ll want to make sure your project is on this version.
Otherwise, I believe all you would need to do is move one of the “entryFile” keys found in either “swagger” and “routes” to the top level of the tsoa.json file and remove it from both entries. Meaning:
{
“entryFile”: “./src/app.ts”,
“swagger”: {
“outputDirectory”: “.”
},
“routes”: {
“basePath”: “/”,
“routesDir”: “./src”
}
}
I *think* this would work.