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 created specific folder for our routes.
- We got router manager express.Router() from the library.
- We got req.params.yourParam from the url.
- We setted a status res.status(200) for every route.
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.