Federico Zacayan

Software Devloper.

Node

Create and go to the /node-tutorial folder.

mkdir node-tutorial && cd node-tutorial

Create an app.js file.

const express = require('express');
const app = express();

app.use((req, res, next) => {
    res.status(200).json({
        message: 'It works!'
    });
});

module.exports = app;

Create an server.js file.

const http = require('http');
const app = require('./app');

const port = process.env.PORT || 3000;
const server = http.createServer(app);

server.listen(port);

Create a folder docker-compose.yml

version: "2"

services:
  node:
    image: "node:8"
    working_dir: /usr/src/app
    environment:
        - NODE_ENV=production #development
    volumes:
      - ./:/usr/src/app
    ports:
      - "3000:3000"
    command: bash -c "npm -y init && npm install --save express"

Run the command below in order to execute automatically 'npm init' into the container (which has node and npm installed) and setting up by default all data required using the flag '-y'.

You can edit those details later.

You are installing express library and all his dependencies as a part to this project too.

sudo docker-compose up

As a result, a package.json file is created.

{
  "name": "app",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node server.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.1"
  }
}

Change the command in docker-compose.yml file.

command: bash -c "npm start"

Now, you can run docker again.

sudo docker-compose up

And test the application on your browser.

http://localhost:3000/

You must install a tool to make requests easily. On of the most popular now is Postman. So, just google it and install it.

If everything is ok you have to get a json file.

{
    message: 'It works!'
}

The console is running a web service on background. You can stop the process typing Ctrl+C.

But, if you run docker-compose in detached mode...

sudo docker-compose up -d

...You would stop the service using down flag instead of the stop flag.

sudo docker-compose down

If you use stop flag.

sudo docker-compose stop

As you can see, the container is has not deleted. It is sleeping.

$ sudo docker container ls -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                     PORTS               NAMES
5bf6d6495020        node:8              "docker-entrypoint.s…"   13 seconds ago      Exited (0) 6 seconds ago                       node-tutorial_node_1

If you use stop flag you can remove the container using the value of the columns "CONTAINER ID" or "NAMES".

Using the NAMES (the last column) it looks like:

sudo docker container rm node-tutorial_node_1

Using the CONTAINER ID it looks like:

sudo docker container rm 5bf6d6495020

Your container can has different values. Do not copy and paste from this tutorial.

Routing

Create a /api folder and the create other one named /api/routes inside.

Then, create /api/routes/orders.js file.

const express = require('express');
const router = express.Router();

router.get('/', (req, res, next) => {
    res.status(200).json({
        message: 'Orders were fetched'
    });
});

router.post('/', (req, res, next) => {
    res.status(201).json({
        message: 'Order was created'
    });
});

router.get('/:orderId', (req, res, next) => {
    res.status(200).json({
        message: 'Order details',
        orderId: req.params.orderId
    });
});

router.delete('/:orderId', (req, res, next) => {
    res.status(200).json({
        message: 'Order deleted',
        orderId: req.params.orderId
    });
});

module.exports = router;

Then, create /api/routes/products.js file.

const express = require('express');
const router = express.Router();

router.get('/', (req, res, next) => {
    res.status(200).json({
        message: 'Handling GET requests to /products'
    });
});

router.post('/', (req, res, next) => {
    res.status(201).json({
        message: 'Handling POST requests to /products'
    });
});

router.get('/:productId', (req, res, next) => {
    const id = req.params.productId;
    if (id === 'special') {
        res.status(200).json({
            message: 'You discovered the special ID',
            id: id
        });
    } else {
        res.status(200).json({
            message: 'You passed an ID'
        });
    }
});

router.patch('/:productId', (req, res, next) => {
    res.status(200).json({
        message: 'Updated product!'
    });
});

router.delete('/:productId', (req, res, next) => {
    res.status(200).json({
        message: 'Deleted product!'
    });
});

module.exports = router;

In app.js replace app.use(...) by the following code.

const productRoutes = require('./api/routes/products');
const orderRoutes = require('./api/routes/orders');

app.use('/products', productRoutes);
app.use('/orders', orderRoutes);

What we did is:

We can test the following test cases.

GET     localhost:3000/products/
GET     localhost:3000/products/1
POST    localhost:3000/products
DELETE  localhost:3000/products/1
PATCH   localhost:3000/products/1

GET     localhost:3000/orders/
GET     localhost:3000/orders/1
POST    localhost:3000/orders/
DELETE  localhost:3000/orders/1

Handling Errors

Instead of shut down the server and run again and again. You would prefer run a tool that do this work automatically.

To do that you can install nodemon.

npm install --save-dev nodemon

Then you have to add some configuration in package.json. You will see "devDependencies" as a last element of this json file.

"devDependencies": {
    "nodemon": "^1.19.2"
}

Then, in the same level of json, you have to write a "scripts" element.

"scripts": {
    ...
    "start": "nodemon server.js"
},

And finally, to automatically stop and restart the server every time there is a change in any file you have to run.

npm start

But unfortunately, this procedure this is not feasible using just docker-compose and we need to use Dockerfile also.

Add a Dockerfile file in the root folder.

FROM node:8

RUN npm install -g nodemon

RUN mkdir -p /usr/src/app

WORKDIR /usr/src/app

COPY package.json .

RUN npm install

CMD [ "npm", "start" ]

Now, replace in docker-compose.yml file

build: . 

Instead of

image: "node:8" 

In this way image is loaded using the Dockerfile file located in the same folder. But, it is able add nodemon as additional layers in the image.

This procedure is useful for you if you do not have node and npm installed in your host machine.

On the other hand, when you are developing you want to know what kind of request you receive in the console.

For it, you can install morgan as a dependency of the project.

Temporarily you can change docker-compose.yml.

command: bash -c "npm install --save morgan"

And then, run up docker-compose.

sudo docker-compose up

After this, you will have added morgan package in you package.json file. And if you have follow this tutorial from scratch all dependencies what we have got are these.

"dependencies": {
    "express": "^4.17.1",
    "morgan": "^1.9.1"
}

Now, we have to roll back docker-compose.yml. The shortcut CTRL+Z CTRL+S could be a good way.

command: bash -c "npm start"

Before running docker-compose again. We need to add in app.js file the morgan dependence as a first middleware.

...
const morgan = require('morgan');
...
app.use(morgan('dev'));//this is the first middleware
app.use(...);//this is the second middleware
app.use(...);//this is the third middleware

If we run the server without using -d we can see the logs on the screen.

Just add the middlewares after Morgan middleware. You can handle the errors placing the middlewares which handle them just at the end.

app.use(...);//this is the second middleware
app.use(...);//this is the third middleware
...
app.use((req, res, next) => {
    const error = new Error('Not found');
    error.status = 404;
    next(error);
})

app.use((error, req, res, next) => {
    res.status(error.status || 500);
    res.json({
        error: {
            message: error.message // 'Not found'
        }
    });
});

As you can see, there is no res (response) sent in the return in any of both error handlers. So, the flow continue to the next middleware.

The last middleware assume there is an error and set the 500 error message.

Now you can run the new functionality.

sudo docker-compose up

You can test the following test cases.

GET     localhost:3000/products/
...
GET     localhost:3000/urlwhichnotexit
GET     localhost:3000/
error.status(404); /*instead of*/ error.status = 404; // restart needed

To allow CORS add the following code before any widdleware which handle routes in app.js.

app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header(
    "Access-Control-Allow-Headers",
    "Origin, X-Requested-With, Content-Type, Accept, Authorization"
  );
  if (req.method === 'OPTIONS') {
      res.header('Access-Control-Allow-Methods', 'PUT, POST, PATCH, DELETE, GET');
      return res.status(200).json({});
  }
  next();
});

As this is a REST API application, we assume the requests would be json. That is why, we need to get and handle the attributes from any body requests received.

We can do it installing body-parser library.

I will assume you understand that to run npm commands you have to modify docker-compose and his command line.

command: bash -c "npm install --save body-parser"
sudo docker-compose up

And after that, rollback.

command: bash -c "npm start"

All that you need to do is import body-parser and add some middlewares before to handle routes in app.js.

...
const bodyParser = require("body-parser");
...
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
...

After that we can access to the params of the body of the request. Check the following hypothetical code.

router.post("/", (req, res, next) => {
  console.log(req.body.name)
});

Now, we need to install mongoose.

command: bash -c "npm install --save mongoose" /* in docker-compose.yml */
sudo docker-compose up

And after that, rollback.

command: bash -c "npm start"

In app.js we need to add the connection.

...
const mongoose = require("mongoose");
...
mongoose.connect(
    "mongodb://admin:secret@mongo:27017/myDatabase",
    {
      useNewUrlParser: true,
      useUnifiedTopology: true
    }
);
mongoose.Promise = global.Promise;
...

You can change the connection string to make this project easier.

"mongodb://mongo:27017/expressmongo" // instead of "mongodb://admin:secret@mongo:27017/myDatabase",

Then, we need to create a folder named model and an api/models/order.js file.

const mongoose = require('mongoose');

const orderSchema = mongoose.Schema({
    _id: mongoose.Schema.Types.ObjectId,
    product: { type: mongoose.Schema.Types.ObjectId, ref: 'Product', required: true },
    quantity: { type: Number, default: 1 }
});

module.exports = mongoose.model('Order', orderSchema);

We need also, an api/models/product.js file.

const mongoose = require('mongoose');

const productSchema = mongoose.Schema({
    _id: mongoose.Schema.Types.ObjectId,
    name: { type: String, required: true },
    price: { type: Number, required: true }
});

module.exports = mongoose.model('Product', productSchema);

Afther the models, we need to add functionality to the routes in api/routes/orders.js.

const express = require("express");
const router = express.Router();
const mongoose = require("mongoose");

const Order = require("../models/order");
const Product = require("../models/product");

// Handle incoming GET requests to /orders
router.get("/", (req, res, next) => {
  Order.find()
    .select("product quantity _id")
    .exec()
    .then(docs => {
      res.status(200).json({
        count: docs.length,
        orders: docs.map(doc => {
          return {
            _id: doc._id,
            product: doc.product,
            quantity: doc.quantity,
            request: {
              type: "GET",
              url: "http://localhost:3000/orders/" + doc._id
            }
          };
        })
      });
    })
    .catch(err => {
      res.status(500).json({
        error: err
      });
    });
});

router.post("/", (req, res, next) => {
  Product.findById(req.body.productId)
    .then(product => {
      if (!product) {
        return res.status(404).json({
          message: "Product not found"
        });
      }
      const order = new Order({
        _id: mongoose.Types.ObjectId(),
        quantity: req.body.quantity,
        product: req.body.productId
      });
      return order.save();
    })
    .then(result => {
      console.log(result);
      res.status(201).json({
        message: "Order stored",
        createdOrder: {
          _id: result._id,
          product: result.product,
          quantity: result.quantity
        },
        request: {
          type: "GET",
          url: "http://localhost:3000/orders/" + result._id
        }
      });
    })
    .catch(err => {
      console.log(err);
      res.status(500).json({
        error: err
      });
    });
});

router.get("/:orderId", (req, res, next) => {
  Order.findById(req.params.orderId)
    .exec()
    .then(order => {
      if (!order) {
        return res.status(404).json({
          message: "Order not found"
        });
      }
      res.status(200).json({
        order: order,
        request: {
          type: "GET",
          url: "http://localhost:3000/orders"
        }
      });
    })
    .catch(err => {
      res.status(500).json({
        error: err
      });
    });
});

router.delete("/:orderId", (req, res, next) => {
  Order.remove({ _id: req.params.orderId })
    .exec()
    .then(result => {
      res.status(200).json({
        message: "Order deleted",
        request: {
          type: "POST",
          url: "http://localhost:3000/orders",
          body: { productId: "ID", quantity: "Number" }
        }
      });
    })
    .catch(err => {
      res.status(500).json({
        error: err
      });
    });
});

module.exports = router;

And the same with api/routes/products.js file.

const express = require("express");
const router = express.Router();
const mongoose = require("mongoose");

const Product = require("../models/product");

router.get("/", (req, res, next) => {
  Product.find()
    .select("name price _id")
    .exec()
    .then(docs => {
      const response = {
        count: docs.length,
        products: docs.map(doc => {
          return {
            name: doc.name,
            price: doc.price,
            _id: doc._id,
            request: {
              type: "GET",
              url: "http://localhost:3000/products/" + doc._id
            }
          };
        })
      };
      //   if (docs.length >= 0) {
      res.status(200).json(response);
      //   } else {
      //       res.status(404).json({
      //           message: 'No entries found'
      //       });
      //   }
    })
    .catch(err => {
      console.log(err);
      res.status(500).json({
        error: err
      });
    });
});

router.post("/", (req, res, next) => {
  const product = new Product({
    _id: new mongoose.Types.ObjectId(),
    name: req.body.name,
    price: req.body.price
  });
  product
    .save()
    .then(result => {
      console.log(result);
      res.status(201).json({
        message: "Created product successfully",
        createdProduct: {
            name: result.name,
            price: result.price,
            _id: result._id,
            request: {
                type: 'GET',
                url: "http://localhost:3000/products/" + result._id
            }
        }
      });
    })
    .catch(err => {
      console.log(err);
      res.status(500).json({
        error: err
      });
    });
});

router.get("/:productId", (req, res, next) => {
  const id = req.params.productId;
  Product.findById(id)
    .select('name price _id')
    .exec()
    .then(doc => {
      console.log("From database", doc);
      if (doc) {
        res.status(200).json({
            product: doc,
            request: {
                type: 'GET',
                url: 'http://localhost:3000/products'
            }
        });
      } else {
        res
          .status(404)
          .json({ message: "No valid entry found for provided ID" });
      }
    })
    .catch(err => {
      console.log(err);
      res.status(500).json({ error: err });
    });
});

router.patch("/:productId", (req, res, next) => {
  const id = req.params.productId;
  const updateOps = {};
  for (const ops of req.body) {
    updateOps[ops.propName] = ops.value;
  }
  Product.update({ _id: id }, { $set: updateOps })
    .exec()
    .then(result => {
      res.status(200).json({
          message: 'Product updated',
          request: {
              type: 'GET',
              url: 'http://localhost:3000/products/' + id
          }
      });
    })
    .catch(err => {
      console.log(err);
      res.status(500).json({
        error: err
      });
    });
});

router.delete("/:productId", (req, res, next) => {
  const id = req.params.productId;
  Product.remove({ _id: id })
    .exec()
    .then(result => {
      res.status(200).json({
          message: 'Product deleted',
          request: {
              type: 'POST',
              url: 'http://localhost:3000/products',
              body: { name: 'String', price: 'Number' }
          }
      });
    })
    .catch(err => {
      console.log(err);
      res.status(500).json({
        error: err
      });
    });
});

module.exports = router;

Finally, add mongodb service to docker-compose.yml

version: "2"

services:
  node:
    build: .
    working_dir: /usr/src/app
    environment:
        - NODE_ENV=production #development
    volumes:
      - ./:/usr/src/app
    ports:
      - "3000:3000"
    command: bash -c "npm start"
    links:
      - mongo
  mongo:
    container_name: mongo
    image: mongo
    ports:
      - "27017:27017"

Hopefully, we have our Api finished.

sudo docker-compose up

Just in case you need to create custom user and database, we present you a simple and easy approach to do it.

Create custom users, passwords and databases

While the docker-compose is running, we will use other console to access to the mongo container.

sudo docker exec -it mongo bash

Into the container go to the mongo client.

mongo

Create a database named myDatabase and move into it with the following command.

use myDatabase

Create a user and password.

db.createUser({ user:'admin', pwd:'secret', roles:['readWrite','dbAdmin'] });

Of course, we do not want to grant it 'dbAdmin' role!

Then, you can finish the mongo client typing 'exit'. Mongo will tell you 'bye'.

> exit
bye
root@c0NtAiN3RiDp:/# 

Then, you can go out from the mongo container typing 'exit' again. The container will tell you 'exit'.

/# exit
exit
api_rest@_with_node:~$ 

But, if you do not want to do all these steps every time you deploy this project the easy way is just use the default mongo docker configuration.