Speed up your RESTful API development in Node.js with Swagger

Free Course

Build Your First Node.js Website

Node is a powerful tool to get JavaScript on the server. Use Node to build a great website.

Introduction

So it happened I started this project with friend of mine, a new app to launch on the mobile market. I offered to write the server-side, a simple web API. Once he got my code he immediately asked me "Sam how do I know the routes and so on?! Write a documentation or something!". And then... Swagger came up.

I presume many of you encountered this situation before or will in the future, a developer may write the best web API service but without proper documentation how can other developers discover and use it? So in this tutorial I want to introduce Swagger, a famous open-source framework to help you write RESTful APIs.

Swagger offers:

  • Interactive documentation.
  • Discoverability.
  • Client SDK generation.

In addition, it is supported by many programming languages.

One feature I like is the mock mode, which allows developers to design routes without writing a single line of code in javascript.

This is the approach I am going to follow in this tutorial: We first focus on the design and then on the code.

In the tutorial we are going create a RESTful API to manage our movies collection.

You can directly clone the app repository from my Github here.

Prerequisites

  • A basic understanding of node.js, how to install packages etc.
  • CRUD cycle.
  • Yaml syntax.

Let's move to the installation of Swagger.

Installation and run the example

As said before Swagger is supported in node.js through its own module, here is the link to the github project: The documentation is pretty straight-forward to help beginners understand and configure Swagger.

Once we open the command line we install the module (global).


npm install -g swagger

Now let's create our project.


swagger project create movie-collection

The prompt asks for the framework we want to use, choose express. Once completed, let's take a look at the project directory:


-- api  
---- controllers 
------ hello_world.js
---- helpers
---- mocks
---- swagger
------ swagger.yaml

-- config 
---- default.yaml

-- test
---- api
------ controllers
-------- hello_world.js
------ helpers

-- app.js
-- package.json

N.B. Each subfolder contains a README.md that for simplicity I didn't include above.

  • api: As the name suggests, everything related to the API development, the controllers, mocks, helpers. A special mention goes to the /swagger folder which contains the file swagger.yaml, an important file we are going to edit to define everything related to the project information and routes. Throughout the tutorial I am going to explain it all so don't worry for now.
  • config: It contains default.yaml that, as the documentation states, drives the application's config directory. Though we are not going to customize the file I still suggest you guys to read the related documentation to understand the engine beneath the "magic" of declaring APIs without writing a line of code but through yaml or json.
  • test: Your test for controllers and helpers are (guess what!) created here.

Obviously, app.js is the main file which runs the server.

Have you noticed that there is already a controller called hello_world.js? Whenever you create a new project, the module adds an example route, a GET request to /hello which takes a name as parameter and greets the person. Not original at all, I give you that, but as we are beginners it is good see Swagger in action and get insight on how it works.

What about taking a look at the example in action?

In order to run the example I have to introduce Swagger editor.

Swagger editor is an elegant browser-based editor which really simplifies our efforts to develop a web API. In particular, it provides:

  • Validation and endpoint routing.
  • Docs on the fly generation.
  • Easy-to-read Yaml.

The picture above shows you the UI of the Swagger editor of our app. On the left you can see the yaml file we are going to edit (swagger.yaml remember?) while on the right the list of routes. By clicking on any of them we can have a good understanding of the parameter required, the format of request and responses, more generally a description of the route.

Now, to launch the example, start the app by running


swagger project start

NB whenever we modify a file, it automatically restarts the server (great!)

Then, open a second command line and launch the editor with:


swagger project edit

If everything went well the editor should open a new tab in your browser. On the right side of the page you should notice the example path for a GET request to /hello, so open the tab and, at the bottom, click on the button try this operation.

We are asked to enter a paramenter name: Go for it and test the route.
Here is my result:

Let's look at the yaml file on the left, precisely on the path part:


paths:
  /hello:
    # binds a127 app logic to a route
    x-swagger-router-controller: hello_world
    get:
      description: Returns 'Hello' to the caller
      # used as the method name of the controller
      operationId: hello
      parameters:
        - name: name
          in: query
          description: The name of the person to whom to say hello
          required: false
          type: string
      responses:
        "200":
          description: Success
          schema:
            # a pointer to a definition
            $ref: "#/definitions/HelloWorldResponse"
        # responses may fall through to errors
        default:
          description: Error
          schema:
            $ref: "#/definitions/ErrorResponse"

This short piece of yaml is what we need to define our routes:

x-swagger-router-controller: This is the controller, the file we have in the /api/controllers/hello_world.js.

Then the http methods must be listed, in the example just a GET.

operationId: This refers to the function, in the controller, in charge of the business logic.

parameters: The list of required parameters are defined here. The parameter name is the only one and you may see that it is in the path, it is not required and it is a string.

When it comes to the responses, Swagger shows its potential:

We can define a different response for each situation, given the http status or errors, a particular response can be defined. In the example, for the status "200" there is a pointer $ref to a response definition while for other statuses, we go to default with its own $ref.
Why is that? In order to keep the yaml file clean, we can define all our response under definitions while at the same time reusing the definition for different responses.

Before we finish with the yaml file, let's take a look at one more piece of code:

consumes:
  - application/json
# format of the responses to the client (Accepts)
produces:
  - application/json

On the top of the file it has defined what the routes consume and produce, it is the format of the request parameters and related response. Those rules are currently applying to all the paths defined in the file. It is also possible to customize these rules for a single path/http method by including these properties inside of it.
However, for the purpose of the tutorial, we will stick to application/json for all the routes, so a single definition on top is more than enough.

Lastly, let's take a look inside /api/controllers/hello_world.js to check the function hello:

function hello(req, res) {
    var name = req.swagger.params.name.value || 'stranger';
    var hello = util.format('Hello, %s', name);
    res.json(hello);
}

Nothing exotic here, though you should notice that with Swagger the req object has a new property swagger which contains the parameters we defined.

In the next section we finally start developing our app.

Working with mocks

Working with mocks does not require any code to be written, just editing the yaml file, so we can focus on the design part first.

First of all, rerun the project adding the flag -m to the command which tells Swagger to run in mock mode, then run the editor in the second window.


swagger movie-collection start -m

GET /movie

The first route returns the complete list of movies in our collection.

Delete the example /hello and add these lines of code:

/movie:
    # our controller name
    x-swagger-router-controller: movie
    get:
      description: get the movies list
      # define the type of response for Success "200" and Error
      responses:
        "200":
          description: Success
          schema:
            $ref: "#/definitions/GetMoviesListResponse"
        default:
          description: Error
          schema:
            $ref: "#/definitions/ErrorResponse"

First, we define our controller movie but there is no need to define the operationId in charge of the business logic. In addition, no need for the parameters. Take note, the successful response has a schema GetMoviesResponse.
The definitions, as usual, are listed at the bottom of the file, so copy and paste this code:

GetMoviesListResponse:
    required:
      - movies
    properties:
      # The array of movies
      movies:
        type: array
        items: 
          type: object
          properties:
            id:
              type: string
            title:
              type: string
            year:
              type: number

The response is nothing but an array of movies which contain a unique id, a title and the year.

NB swagger editor autosaves whatever we input, which is great for us, but I noticed that sometimes is gets stuck. So if that is the case for you, try to refresh the page.

Here is the code inside the editor which automatically created a better view on the right.

Now test the route and check the result:

Since we didn't define any operationId, in mock mode swagger returns a standard value according to the properties type.
Since id and title are strings, it returns "string" while for the year, as a number, it returns 1.

For the record, if not satisfied by the standard values, it is always possible to define the mock controller in /api/mocks and add a operationId. Then, the business logic just returns the answer in the format you defined in the definitions, but you can customize the values for each property.
However, I am not going to cover this step because I want to stick to the promise of working with mocks without writing a single line of code.

POST /movie

Let's create a route to add a new movie to the list. After get: add the following yaml lines:


 post:
      description: add a new movie to the list
      # movie info to be stored
      parameters:
        - name: title
          description: Movie properties
          in: body
          required: true
          schema:
            $ref: "#/definitions/Movie"
      responses:
        "200":
          description: Success
          schema:
            $ref: "#/definitions/GeneralResponse"
        default:
          description: Error
          schema:
            $ref: "#/definitions/ErrorResponse"

This time we have a parameter in the body which is simply the movie object with a title and year. Let's add the schema in the definitions:


Movie:
    type: object
    properties:
      title:
        type: string
        description: task object name
      year:
        type: number
        description: task description
    required:
      - title
      - year

NB We declared the two fields title and year as required.

What about the responses? The error response is always the same (reusability) while we defined a GeneralResponse. So, copy this code in the definition:

GeneralResponse:
    type: object
    properties:
      success:
        type: number
        description: returns 1 if successful
      description:
        type: string
        description: a short comment 
    required:
      - success
      - description

So to speak, we have a success field set to 1 and a description of the operations. This will be a common response for all our routes when we create or update a movie, the only difference will be the description text.

Let's try to add a movie:

Here is the result:

GET /movie/{id}

Now let's retrieve a single movie. This time we need the parameter in the path, precisely the unique id.

We first need to create a new path called /movie/{id} so copy and paste the following inside of it:

/movie/{id}:
    # our controller name
    x-swagger-router-controller: movie
    get:
      description: get a movie
      # define the type of response for Success "200" and Error
      parameters:
        - name: id
          type: string
          in: path
          required: true
      responses:
        "200":
          description: Success
          schema:
            $ref: "#/definitions/GetMovieResponse"
        default:
          description: Error
          schema:
            $ref: "#/definitions/ErrorResponse"

Notice that this time the parameter, in its own field in has path as value.

The successful response it's the movie object itself, so we need to add GetMovieResponse to the list of definitions:

GetMovieResponse:
    required:
      - id
      - title
      - year
    properties:
      id:
        type: string
      title: 
        type: string
      year:
        type: number
  Movie:
    type: object
    properties:
      title:
        type: string
        description: task object name
      year:
        type: number
        description: task description
    required:
      - title
      - year

Let's try it, for the id parameter, we can mock it with a fake one:

and here is the expected response:

PUT /movie/{id}

Now it's time create the route to update a movie, given the id in the path and a new title and year in the body. Copy the following code in the editor:

put:
      description: update a movie
      # define the parameters
      parameters:
        - name: id
          description: Movie id
          type: string
          in: path
          required: true
        - name: title
          description: Movie properties
          in: body
          required: true
          schema:
            $ref: "#/definitions/Movie"
      responses:
        "200":
          description: Success
          schema:
            $ref: "#/definitions/GeneralResponse"
        default:
          description: Error
          schema:
            $ref: "#/definitions/ErrorResponse"

I really like the order to define the parameters, no matter if they come from the body, path, header etc. They are all consequently listed where in assumes the correct value.

Notice the GeneralResponse, the same schema we used for adding a new movie

So, update a file:

And the result:

DELETE /movie/{id}

The last operation, let's delete a movie by a given id.

Copy and past the following code:

delete:
      description: delete a movie
      # define the parameters
      parameters:
        - name: id
          description: Movie id
          type: string
          in: path
          required: true
      responses:
        "200":
          description: Success
          schema:
            $ref: "#/definitions/GeneralResponse"
        default:
          description: Error
          schema:
            $ref: "#/definitions/ErrorResponse"

There is no need to further discuss the yaml file, we simply define the parameter and the response is GeneralResponse.

Try the operation:

Here is the result:

...And congratulations! We finished the design and now it's about time to write some javascript code.

Implement the controller and run the real app

The first thing to implement is the data-storage logic. Well, for the purpose of the tutorial we are not going to use any real database (MySQL, Mongo...) but simply store the data in memory for the time the app runs.

We need to add a new package, crypto, to create the unique id for any stored movie:

npm install crypto --save

Let's create a file db.js /config. We are going to export a simple object which mocks the DB operations of add, retrieve, update and delete movies.

Here is the code to copy

'use strict;'
//Include crypto to generate the movie id
var crypto = require('crypto');

module.exports = function() {
    return {
        movieList : [],
        /*
         * Save the movie inside the "db".
         */
        save(movie) {
            movie.id = crypto.randomBytes(20).toString('hex'); // fast enough for our purpose
            this.movieList.push(movie);
            return 1;           
        },
        /*
         * Retrieve a movie with a given id or return all the movies if the id is undefined.
         */
        find(id) {
            if(id) {
                return this.movieList.find(element => {
                        return element.id === id;
                    }); 
            }else {
                return this.movieList;
            }
        },
        /*
         * Delete a movie with the given id.
         */
        remove(id) {
            var found = 0;
            this.movieList = this.movieList.filter(element => {
                    if(element.id === id) {
                        found = 1;
                    }else {
                        return element.id !== id;
                    }
                });
            return found;           
        },
        /*
         * Update a movie with the given id
         */
        update(id, movie) {
            var movieIndex = this.movieList.findIndex(element => {
                return element.id === id;
            });
            if(movieIndex !== -1) {
                this.movieList[movieIndex].title = movie.title;
                this.movieList[movieIndex].year = movie.year;
                return 1;
            }else {
                return 0;
            }
        }       
    }
};  

The names of the functions are the same as the traditional operations with Mongoose. The first property is a list to store all the movies in the collection.

  • Save: We need to create a unique id, here is where crypto comes to help.
  • Find: If the id is passed, then it returns the specific movie, all the list otherwise.
  • Remove: The .filter method of javascript arrays does pretty much what we need, remove and return the found movie.
  • Update: Update the movie if it exists in the collection.

NB it is very likely some of you may find better ways to implement the operations above, or even point out that I should return a shallow copy of the list instead of the property itself. Because this is just a simple demo to fake CRUD operations, I would suggest you customize my object for your needs or even connect to a real database.

Movie controller

In /api/controllers create movie.js and paste the following code:

 'use strict';
    // Include our "db"
    var db = require('../../config/db')();
    // Exports all the functions to perform on the db
    module.exports = {getAll, save, getOne, update, delMovie};

    //GET /movie operationId
    function getAll(req, res, next) {
      res.json({ movies: db.find()});
    }
    //POST /movie operationId
    function save(req, res, next) {
        res.json({success: db.save(req.body), description: "Movie added to the list!"});
    }
    //GET /movie/{id} operationId
    function getOne(req, res, next) {
        var id = req.swagger.params.id.value; //req.swagger contains the path parameters
        var movie = db.find(id);
        if(movie) {
            res.json(movie);
        }else {
            res.status(204).send();
        }       
    }
    //PUT /movie/{id} operationId
    function update(req, res, next) {
        var id = req.swagger.params.id.value; //req.swagger contains the path parameters
        var movie = req.body;
        if(db.update(id, movie)){
            res.json({success: 1, description: "Movie updated!"});
        }else{
            res.status(204).send();
        }

    }
    //DELETE /movie/{id} operationId
    function delMovie(req, res, next) {
        var id = req.swagger.params.id.value; //req.swagger contains the path parameters
        if(db.remove(id)){
            res.json({success: 1, description: "Movie deleted!"});
        }else{
            res.status(204).send();
        }

    }

We first included our db and then exported all the functions we need to use as operationId and handled the business logic.

Notice that in the case that the id belongs to a movie, getOne, update, and delMovie return the same formatted json. We defined the same schema in the Swagger editor earlier.
On the other hand, in case there is no movie with the id of the request, we send a status of no content "204".

Now rerun the project without the -m flag and in the editor we have to add the operationId to each route, right after the "verb":

    #in /movie
    get:
      operationId: getAll
    #in /movie
    post:
      operationId: save
    #in /movie/{id}
    get:
      operationId: getOne
    #in /movie/{id}
    put:
      operationId: update
    #in /movie/{id}
    get:
      operationId: delMovie

Add a movie

The same as before, add a movie

Result:

Get all the movies

After we added more movies, let's try retrieve them all by a GET request to /movie:

Side note, a true Star Wars fan must have noticed that year of "The Empire Strikes Back" is wrong, it was released in 1980. In addition I forgot a "s" in the title.

Good, time to update the movie!

Update a movie

Add the id as a parameter in the PUT request to /movie/{id}

Result:

Then, let's doublecheck the movie is really updated

Get a single movie

We need a GET request to /movie/{id} with the movie id:

Result:

It works!

Delete a movie

Let's first get all the movies with a GET request to /movie:

We decide to delete "The force awakens", so let's send a DELETE request to /movie/{id} with the right id:

Result:

Finally, let's double check whether it was really deleted or not by sending a GET request to /movie/{id}:

The NOCONTENT response confirms it all worked.

Conclusions

In this tutorial we have seen how Swagger helps to develop a robust RESTful API while at the same time providing an elegant documentation that can boost up cooperation among developers (for example backend and frontend developers).

We covered all the basic requests, GET, POST, DELETE, UPDATE to manage a movie collection. At the same time we practiced with parameters by handling them from the body or the path.
The same can be done to handle parameters from the header of course, so I have a challenge for you:

On Scotch.io there is a cool tutorial to authenticate a node.js API with JSON web tokens. Why don't you try to rewrite with swagger to practice with header parameters?

Last but not the least, I remind you again to take a look a the documentation on the official website for a deeper understanding of Swagger.

Samuele Zaza

I am a full-stack web developer working for Taroko Software as front-end web developer and Filestack Tech Evangelist. When not coding I may be spotted in a gym lifting or planning to conquer the world LOL.