Routing An OpenAPI Application With Node Express And Swagger

Routing An OpenAPI Application With Node Express And Swagger

How to organize your OpenAPI files

Prologue

I recently learned about how to use swagger-cli to to create a node application and I was eager to try it with a new project. I was quickly confronted with some really tough issues and like the saying goes when you are confronted with an issue you either flee or fight. In my case I decided to not flee but fight it head-on.

Main Issue With The Package

The package that the swagger-cli builds the project with has a main issue of not being able to use the $ref for components that are defined outside of the yaml file, at least, that's the impression I get from the github discussions that spans so many years back without a true fix. This was because of it's dependence on some other packages and another and the story continues. For me, I was really bent on getting that simple functionality of not having to create and configure separate routers for my project. I really wanted to achieve that kind of communication between the OpenAPI file and the controller methods and for this simple interest I found out the swagger-routes-express npm package which seems to be like the backbone for this to be achieved.

In this tutorial I would show you other things I picked up and how I had to re-architect my project setup from my previous blog post on Integrating OpenAPI with Node . I would also share with you my opensource repo which comes with everything that will be discussed in this article already bootstrapped.

Prerequisites

You should have a working knowledge of

  • Node
  • Express
  • OpenAPI with Swagger

Project Setup

First, let's install a few packages that are useful for this project.

  • Nodemon for reloading our server when we make changes
  • Swagger Parser a dependency that's required by swagger-routes-express
  • Swagger UI Express for the Swagger UI functionalities
  • Swagger Routes Express for handling routing
npm install nodemon swagger-parser swagger-routes-express swagger-ui-express

After the packages are successfully installed, we will go ahead to create 2 files in our root folder, app.js and server.js. This is just for separation of concerns. If you have just a single file serving as the entry point and for your express configs too that's also fine so long as you are able to follow along. Inside the app.js file paste this code

const express = require('express');
const swaggerUi = require('swagger-ui-express');
const swaggerParser = require('swagger-parser')
const { connector } = require('swagger-routes-express');
const api = require('./api');

const makeApp = async () => {
  const parser = new swaggerParser();
  // this is the path to your OpenAPI file
  const apiDefinition = await parser.validate('./docs/openapi.yaml');
  const connect = connector(api, apiDefinition) // make the connector
  const app = express() // make the app
  // do any other app stuff, such as wire in passport, use cors etc

  // This is the endpoint that will display the swagger docs
  app.use('/api-docs',swaggerUi.serve, swaggerUi.setup(apiDefinition));

  connect(app); // attach the routes

  // add any error handlers last

  return app;
}
module.exports = makeApp;

Inside the server.js paste this code

const makeApp = require('./app');
const port = 3000;

makeApp()
  .then(app => {
    // use app to do other express related configs here
    app.listen(port)
  })
  .then(() => {
    console.log(`App running on port ${port}...`)
  })
  .catch(err => {
    console.error('Caught an error', err)
});

Optionally, we are creating a nodemon.json in the root of our project to hold the file extensions that should cause a restart of the server. This is because by default, nodemon would not watch for changes in .yaml files. Add this code to the nodemon.json file

{
  "ext": ".js, .mjs, .coffee, .litcoffee, .json, .yaml"
}

Organizing files

Another very important part of this project is how we organize our files. First, so that the routing will work as expected and second, so that when our app grows we can easily manage our API docs. Inside the root of our application we are going to create 2 main folders, api and docs. It could be more but these 2 are mandatory for this tutorial. Inside the api folder, we will create a folder called controllers and an index.js file. The controllers folder will do as the name says, hold the controllers of our application. The purpose of the index.js file is to hold all the controller methods that will be exported from the various controller files that will be created. This is very important for the routing to work because we are importing const api = require('./api'); inside app.js which is is not pointing to any file in our folder. Node will look for an index.js and that's how come we are able to achieve that form of import in our Node apps. So anytime you want to import your own folders without specifying a file remember to add an index.js file in the root of that folder.

Back to where we left off. Now, inside our docs which will hold everything related to the API documentation, we will create the openapi.yaml file. We will then create these folders

  • schemas
  • responses
  • paths

because we don't want to have one giant OpenAPI file which will be difficult to read when our project grows in the future.

OpenAPI setup

Now that we are done organizing our files, we go ahead to start working on our API documentation. Inside ./docs/openapi.yaml paste this code and adjust it to your needs

openapi: 3.0.0
info:
  description: Your API Description here
  version: 1.0.0
  title: API Docs Title
servers:
  - url: /api/v1

paths:
  /users:
    $ref: "./paths/users.yaml"

If you watch closely at the paths section we are referencing an external file so we obviously have to create the users.yaml file in our ./docs/paths and add this code below

get:
  summary: Returns a list of users.
  operationId: getAllUsers #This is the function that would be called from ./api/controllers/index.js
  description: Optional extended description in CommonMark or HTML
  tags:
    - User
  responses:
    '200':
      description: A JSON array of user names
      content:
        application/json:
          schema: 
            $ref: "../schemas/user.yaml"
    default:
      $ref: "../responses/server-error.yaml"

There are couple of things that we can glean from this code. We will create a generic component that will be used to handle all our server errors. Inside the ./docs/responses/server-error.yaml we will add this code

description: An unexpected error was encountered
content:
  application/json:
    schema:
      $ref: "../schemas/error.yaml"

Now let's create all the schemas we are using inside our ./docs/schemas folder. Create user.yaml and paste this code

type: object
properties:
  username:
    type: string
  email:
    type: string
    format: email

required:
  - email

Create error.yaml and paste this code

type: object
properties:
  message:
    type: string
  code:
    type: string

Routing

The point with our routing here is that the value we specify for operationId in our paths should correspond to an actual controller function and actually execute the function should we hit that endpoint. We already have our folder structures already in place and setup done so all we have to do is just a couple of steps. Inside ./api/controllers/users-controller.js add your controller function as below

exports.getAllUsers = async (req, res, next) => {
  const usersList = [
    { username: 'Reuben', email: 'reuben@example.com' }, 
    { username: 'Frimpong', email: 'frimpong@example.com' },
  ];
  res.json(usersList);
}

A very key point to do here is inside ./api/index.js we would have import the controller functions and export them so that the swagger-routes-express can use them. Add this code

const { getAllUsers } = require('./controllers/users-controller');

module.exports = {
  getAllUsers,
}

Remember it's important that the name of the function matches the name that was specified in the operationId of the paths section of our OpenAPI doc.

If you followed me step-by-step your folder structure should look something like this, ignoring the .env file Screenshot from 2021-10-17 16-18-08.png

Running Our App

It is finally time for us to run our app. Inside your package.json file add this command to your scripts section

"start": "nodemon server.js",

Remember to save all the work you have done and run the server with

npm run start

Inside your browser visit the url http://localhost:3000/api-docs to view your API documentation which would look like below

Screenshot from 2021-10-18 10-28-00.png

Next, we have to test our API to see if truly it works as we expect it. For that we would use Postman (we can also do this in the Swagger UI by clicking the Try It Out button). Inside our Postman we would create a new GET request to the http://localhost:3000/api/v1/users endpoint and it should return the some data from our controller like below

Screenshot from 2021-10-18 10-32-11.png

This is amazing as you can see. The routes are hitting the exact endpoint and behaves just like we want it.

Conclusion

I know this was a lot of steps to follow but you don't need to do all the steps. I have already created working skeleton on GitHub that you could just clone, modify and use. Find the repo here . Feel free to connect with me here on Hashnode, LinkedIn, and Twitter . I'm always happy to hear from you.