Another ExpressJS API tutorial for 2021, part 09 — Docker!
Hi guys, in this article we are going to configure our API made in ExpressJS and Typescript to run in a Docker container!

Hi there, if it is your first time arriving here we are creating a series on how to build an API using ExpressJS and Typescript. The first article can be found here.
Just as a quick overview, here is the series of articles that we are writing:
- Create your hello world ExpressJS API (Yes… we always need to start with a hello world)
- Configure our hello world application to use Typescript (TS is cool, believe me ;) )
- Create our first CRUD API endpoints for an user (Everyone wants a registered user in their applications, no?)
- Create and configure your controllers (We will talk about it after)
- Create and configure your services (What the h. is that?)
- Middleware’s! (Yes, we need and use it everyday)
- End to end testings!
- Configuring a secure way to manage user permissions via API
- Putting all together into a Docker container — We are here now ;)
- Configuring a simple MongoDB to connect to our application
- Configuring permission level to the application
- Add logs with Winston!
So today we are going to pause a bit our API coding to integrate our project into a Docker container. The main reason of doing this now are:
- We want to use a real database for the next article;
- It’s difficult to know which IOS you reader are using (Windows, Mac, Linux, …)
- You already have a lot of the API working and it is fun to test it in a Docker container
- With a docker container, we can make sure that what we do at our local machine should work exactly in a real environment
That all said, what is a Docker container?
A container is a standard unit of software that packages up code and all its dependencies so the application runs quickly and reliably from one computing environment to another. A Docker container image is a lightweight, standalone, executable package of software that includes everything needed to run an application: code, runtime, system tools, system libraries and settings. (Docker site)
Well, this is the starting basic idea but if you are more interested in learning Docker, you can start at this medium article
To start, please download the docker app for your OS and install using this link.
After installing, we will start to configure our files. Please make sure to use our latest GitHub branch that you can find it here.
We will give a standard receipt that works for a NodeJS application. To start, we want to have 3 files:
- .dockerignore: that works similar to .gitignore
- Dockerfile: here we will describe how to run our application image
- docker-composer.yml: here we can configure how can we run one or more containers at the same time and make them to be able to communicate with each other
Dockerfiles describe how to assemble a private filesystem for a container, and can also contain some metadata describing how to run a container based on this image (Docker site)
Now, let’s create these three files at our root project folder. Remember that if there is no extension, you should not add .txt or something. The name of the files are exactly like: .dockerignore; Dockerfile; docker-composer.yml
Docker Composer: Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application’s services (Docker site)
The dockerignore file: for now we just want to make sure that our container will not use the entire node_modules to save space at our container.
node_modulesThe Dockerfile: here we are using the Dockerfile commands to use a ready image from the Docker hub. For now we are using node:10-slim but make sure to search about how to use Alpine images if you need to save a lot of memory in your live application.
FROM node:10-slimRUN mkdir -p /usr/src/appWORKDIR /usr/src/appCOPY . .RUN npm installEXPOSE 3000CMD ["node", "./build/app.js"]For using the Dockerfile there is no much to tell, the algorithm is pretty simple: grab an image, create a folder, use this folder, copy everything of our root project (except node_modules), run npm install, make our port 3000 visible and run our builded app. If it is your first time using a Dockerfile, please read their docs here to understand it better.
The docker-composer.yml file: here we will start the configuration that will be used in the next article when we will start a MongoDB server without installing it at our OS ;)
version: '3'
services:
api:
command: npm run test-dev
build: .
volumes:
- ./api:/app
networks:
- backend
ports:
- "3000:3000"networks:
backend:
driver: bridgeFor now just copying and pasting this “formula” would work to run our app at the port 3000. Again, if it is your first time using Docker and docker-composer, please make sure to read their documentation here. Just talking about all the magic behind of Docker, Dockerfile and docker-composer are enough of topic to several articles but let it make things simple here.
Let’s run and test it
Since we had made a configuration with docker-composer, here is how we would handle it:
At your root project, via terminal, run the following:
docker-compose buildYou should expect something like:
Building apiStep 1/7 : FROM node:10-slim---> 4bc78f8574a2Step 2/7 : RUN mkdir -p /usr/src/app---> Using cache---> 807e720c3b34Step 3/7 : WORKDIR /usr/src/app---> Using cache---> 58e8a3eb3d25Step 4/7 : COPY . .---> Using cache---> 0fd49896f7a8Step 5/7 : RUN npm install---> Using cache---> b2b6afefa85dStep 6/7 : EXPOSE 3000---> Using cache---> 0de68cccb581Step 7/7 : CMD ["node", "./build/app.js"]---> Using cache---> 45d29807c034Successfully built 45d29807c034Successfully tagged expressjs-api-tutorial_api:latestNow, we have our build we can run the application. Run the command:
docker-compose upAnd, we should expect something like:
Starting expressjs-api-tutorial_api_1 ... doneAttaching to expressjs-api-tutorial_api_1api_1 |api_1 | > expressjs-api-tutorial@0.0.1 test-dev /usr/src/appapi_1 | > nodemon --watch . --ext ts --exec "mocha -r ts-node/register test/**/*.test.ts"api_1 |api_1 | [nodemon] 2.0.2api_1 | [nodemon] to restart at any time, enter `rs`api_1 | [nodemon] watching dir(s): *.*api_1 | [nodemon] watching extensions: tsapi_1 | [nodemon] starting `mocha -r ts-node/register test/**/*.test.ts`api_1 | Server running at port 3000api_1 | Routes configured for UsersRouteapi_1 | Routes configured for UsersRouteapi_1 |api_1 |api_1 | Created new instance of GenericInMemoryDaoapi_1 | ✓ should POST /users (118ms)api_1 | ✓ should POST to /auth and retrieve an access token (81ms)api_1 | ✓ should POST to /auth/refresh-token and receive 403 for having an invalid JWTapi_1 | ✓ should POST to /auth/refresh-token and receive 401 for not having a JWT setapi_1 | ✓ should POST to /auth/refresh-token and receive 400 for having an invalid refreshTokenapi_1 | ✓ should POST to /auth/refresh-token and retrieve a new access tokenapi_1 | ✓ should DELETE /users/:userIdapi_1 | ✓ should POST /users (76ms)api_1 | ✓ should GET /users/:userIdapi_1 | ✓ should GET /usersapi_1 | ✓ should PUT /users/:userIdapi_1 | ✓ should GET /users/:userId to have a new nameapi_1 | ✓ should PATCH /users/:userIdapi_1 | ✓ should GET /users/:userId to have a new field called descriptionapi_1 | ✓ should DELETE /users/:userIdapi_1 | ✓ should GET /usersapi_1 |api_1 | 16 passing (318ms)And that it! We have our application running inside a container and also running our test integration to keep coding safely!
Ok, ok… but how can I keep running tests meanwhile coding with a docker-compose file? That is quite simple, let’s add a script to our scripts at the package.json as the following:
"dev": "nodemon --watch . --ext ts --exec \"docker-compose build && docker-compose up\"",Or, if you are that lazy, just copy and paste the updated package.json as the following:
{
"name": "expressjs-api-tutorial",
"version": "0.0.1",
"description": "Tutorial of how to create a REST API using ExpressJS",
"main": "dist/app.js",
"scripts": {
"tsc": "tsc",
"start": "npm run tsc && node ./dist/app.js",
"test-dev": "nodemon --watch . --ext ts --exec \"mocha -r ts-node/register test/**/*.test.ts\"",
"dev": "nodemon --watch . --ext ts --exec \"docker-compose build && docker-compose up\"",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/makinhs/expressjs-api-tutorial.git"
},
"keywords": [
"REST",
"API",
"ExpressJS",
"NodeJS"
],
"author": "Marcos Silva",
"license": "ISC",
"bugs": {
"url": "https://github.com/makinhs/expressjs-api-tutorial/issues"
},
"homepage": "https://github.com/makinhs/expressjs-api-tutorial#readme",
"dependencies": {
"argon2-pass": "^1.0.2",
"express": "^4.17.1",
"jsonwebtoken": "^8.5.1",
"short-uuid": "^3.1.1"
},
"devDependencies": {
"@types/chai": "^4.2.8",
"@types/express": "^4.17.2",
"@types/mocha": "^7.0.1",
"@types/mongoose": "^5.7.0",
"@types/node": "^13.5.2",
"@types/supertest": "^2.0.8",
"chai": "^4.2.0",
"mocha": "^7.0.1",
"nodemon": "^2.0.2",
"source-map-support": "^0.5.16",
"supertest": "^4.0.2",
"ts-node": "^8.6.2",
"tslint": "^6.0.0",
"typescript": "^3.7.5"
}
}Ok, now instead of running docker commands, at the root project with your terminal we should run the following:
npm run devYou should expect to have a build then running application as a magic :) Now just change any part of your code, save and see it running again, like magic.
This article we configured a Docker container and Composer to run our API inside a container. At our next chapter we will configure a MongoDB and finally change the in memory DAO to something that make sense in the real life.
Thanks for reading and see you at the next article!
tips:
- the full code of this article is here
- don’t forget to use a nice app to test your API such as Postman or Insomnia
- I know you still didn’t… Read the ExpressJS documentation!
- Learn Docker, start at their Documentation!
- Complete project is here! (Includes even the non existing articles yet)
