Build an API using Node, Express, MongoDB, and Docker
Tags: docker, node, javascript, typescript
Published: 2021-10-17 16:46:08

In this tutorial, we'll be using TypeScript and Docker to build a Menu Restaurant API app from scratch with Node, Express, and MongoDB. The Docker part is optional.

Basically, we should be able to: 
- retrieve all menus
- retrieve one menu
- create a menu
- update a menu
- delete a menu

Great, let's dive in.

## Setting up

To create a new Nodejs project, we'll first run this command on the terminal. 

```
yarn init
```

It'll ask a couple of questions before initializing the project. Anyway, you can bypass this by adding a `-y` flag to the command.

Next step is to create a structure for our project. 

```shell
├── dist
├── src
   ├── app.ts
   ├── controllers
   |  └── menus
   |     └── index.ts
   ├── models
   |  └── menu.ts
   ├── routes
   |  └── index.ts
   └── types
      └── menu.ts
├── nodemon.json
├── package.json
├── tsconfig.json
```
Let me quickly explain the structure of the project. 
- `dist` will serve as the output folder once the typescript code is compiled to plain JavaScript.
- `src` will contains the logic of our API. 
 - `app.ts` is the entry point of the server.
 - `controllers` will contain functions that handle requests and return data from the model to the client
 - `models` will contain objects that will allow basic manipulations with our database.
- `routes` are used to forward the requests to the appropriate controller.
- `types` will contain the interface of our objects in this project.

To continue, let's add some configurations to `tsconfig.json`. This will help the computer along following our preferences for development. 

```json
{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "dist/js",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["src/types/*.ts", "node_modules", ".vscode", ".idea"]
}
```
Now we can start installing the dependencies to start our project. But first, let's enable TypeScript. 

```
yarn add typescript
```

Let's also add some dependencies to use Express and MongoDB.

```
yarn add express cors mongoose
```

Next, we'll be adding their types as development dependencies. This will help the TypeScript computer understanding the packages.

```
yarn add -D @types/node @types/express @types/mongoose @types/cors
```
Let's add some dependencies for auto-reloading the server when a file is modified and start the server concurrently (We'll be able to make changes and start the server simultaneously). 

```
yarn add -D concurrently nodemon
```

We need to update the `package.json` file with the scripts needed to start the server and build the project.
Here's how your `package.json` file should look.

```json
{
  "name": "menu-node-api",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.17.1",
    "mongoose": "^6.0.11",
    "nodemon": "^2.0.13",
    "typescript": "^4.4.4"
  },
  "scripts": {
    "build": "tsc",
    "start": "concurrently \"tsc -w\" \"nodemon dist/js/app.js\""
  },
  "devDependencies": {
    "@types/cors": "^2.8.12",
    "@types/express": "^4.17.13",
    "@types/mongoose": "^5.11.97",
    "@types/node": "^16.11.1",
    "concurrently": "^6.3.0"
  }
}
```
The project is ready. We can start coding now. :)

## Building the API

Here's how we'll be working: 
- Creating the Menu type
- Creating the Menu model
- Creating the Menu controllers
- Adding the Menu routes
- Configuring `app.ts` to connect to Mongo Atlas and start the server.

### Creating the Menu type

We'll be writing a Menu interface that'll be extending the `Document` type provided by `mongoose`. It'll be useful to interact with MongoDB later.

```typescript
import { Document } from "mongoose";

export interface IMenu extends Document {
  name: string;
  description: string;
  price: number;
}
```

### Creating a Menu Model

```typescript
import { IMenu } from "../types/menu";
import { model, Schema } from "mongoose";

const menuSchema: Schema = new Schema(
  {
    name: {
      type: String,
      required: true,
    },
    description: {
      type: String,
      required: true,
    },
    price: {
      type: String,
      required: true,
    },
  },
  { timestamps: true }
);

export default model<IMenu>("Menu", menuSchema);
```

`mongoose` provides with helpful utilities to create a model. Notice that here `IMenu` is used as a type for the model before exporting it. 

Now that the model is written, we can start interacting with the database on other files.

### Creating the controllers

We'll be writing 5 controllers here. 

- `getMenus`: To get all the menu objects in the database
- `addMenu`: To create a Menu
- `updateMenu`: To update a Menu
- `deleteMenu`: To delete a Menu
- `retrieveMenu`: To retrieve a Menu

Let's start with `getMenus`.

```typescript
// ./src/controllers/menus/index.ts

import { Response, Request } from "express";
import { IMenu } from "../../types/menu";
import Menu from "../../models/menu";

const getMenus = async (req: Request, res: Response): Promise<void> => {
  try {
    const menus: IMenu[] = await Menu.find();
    res.status(200).json({ menus });
  } catch (error) {
    throw error;
  }
};
```

Firstly, we are importing `Request` and `Response` types from `express` to type the values explicitly. Next step, the `getMenus` function is created to fetch data from the database.

- It receives a `req` and `res` parameters and returns a promise
- And with the help of the `Menu` model created earlier, we can now retrieve all the `menus` from MongoDB and return a response containing these objects.

Great, let's move to the `addMenu` controller.

```typescript
const addMenu = async (req: Request, res: Response): Promise<void> => {
  try {
    const body = req.body as Pick<IMenu, "name" | "description" | "price">;
    const menu: IMenu = new Menu({
      name: body.name,
      description: body.description,
      price: body.price,
    });

    const newMenu: IMenu = await menu.save();

    res.status(201).json(newMenu);
  } catch (error) {
    throw error;
  }
};
```

A bit different from `getMenus`, this function now receives a body object that will contain data entered by the user.

Next, we use typecasting to avoid types and make sure the `body` variable matches `IMenu`, and then we create a new `Menu` and then save the `Menu` in the database.

```typescript
const retrieveMenu = async (req: Request, res: Response): Promise<void> => {
  try {
    const {
      params: { id },
    } = req;
    const menu: IMenu | null = await Menu.findById({ _id: id });

    res.status(menu ? 200 : 404).json({ menu });
  } catch (error) {
    throw error;
  }
};
```

This function will pull out the `id` from the `req` object and then pass it as an argument to the `findById` method to access the object and return it to the client.

```typescript
const updateMenu = async (req: Request, res: Response): Promise<void> => {
  try {
    const {
      params: { id },
      body,
    } = req;

    const updateMenu: IMenu | null = await Menu.findByIdAndUpdate(
      { _id: id },
      body
    );

    res.status(updateMenu ? 200 : 404).json({
      menu: updateMenu,
    });
  } catch (error) {
    throw error;
  }
};
```

This function accepts an `id` parameter but also the `body` object.
Next, we use the `findByIdAndUpdate` to retrieve the corresponding Menu from the database and update it.

```typescript
const deleteMenu = async (req: Request, res: Response): Promise<void> => {
  try {
    const deletedMenu: IMenu | null = await Menu.findByIdAndRemove(
      req.params.id
    );
    res.status(204).json({
      todo: deletedMenu,
    });
  } catch (error) {
    throw error;
  }
};
```

This function allows us to delete a Menu from the database.
Here, we pull out the `id` from `req` and pass it as an argument to `findByIdAndRemove` method to access the corresponding Menu and delete it from the database.

We have the controllers ready and let's export them. 

Here's the final code of the `src/controllers/menus/index.ts` file.

```typescript
import { Response, Request } from "express";
import { IMenu } from "../../types/menu";
import Menu from "../../models/menu";

const getMenus = async (req: Request, res: Response): Promise<void> => {
  try {
    const menus: IMenu[] = await Menu.find();
    res.status(200).json({ menus });
  } catch (error) {
    throw error;
  }
};

const retrieveMenu = async (req: Request, res: Response): Promise<void> => {
  try {
    const {
      params: { id },
    } = req;
    const menu: IMenu | null = await Menu.findById({ _id: id });

    res.status(menu ? 200 : 404).json({ menu });
  } catch (error) {
    throw error;
  }
};

const addMenu = async (req: Request, res: Response): Promise<void> => {
  try {
    const body = req.body as Pick<IMenu, "name" | "description" | "price">;
    const menu: IMenu = new Menu({
      name: body.name,
      description: body.description,
      price: body.price,
    });

    const newMenu: IMenu = await menu.save();

    res.status(201).json(newMenu);
  } catch (error) {
    throw error;
  }
};

const updateMenu = async (req: Request, res: Response): Promise<void> => {
  try {
    const {
      params: { id },
      body,
    } = req;

    const updateMenu: IMenu | null = await Menu.findByIdAndUpdate(
      { _id: id },
      body
    );

    res.status(updateMenu ? 200 : 404).json({
      menu: updateMenu,
    });
  } catch (error) {
    throw error;
  }
};

const deleteMenu = async (req: Request, res: Response): Promise<void> => {
  try {
    const deletedMenu: IMenu | null = await Menu.findByIdAndRemove(
      req.params.id
    );
    res.status(204).json({
      todo: deletedMenu,
    });
  } catch (error) {
    throw error;
  }
};
export { getMenus, addMenu, updateMenu, deleteMenu, retrieveMenu };
```

### API routes

We'll be creating five routes to get, create, update and delete menus from the database. We'll be using the controllers we've created and pass them as parameters to handle the requests when defining the routes.

```typescript
import { Router } from "express";
import {
  getMenus,
  addMenu,
  updateMenu,
  deleteMenu,
  retrieveMenu,
} from "../controllers/menus";

const menuRoutes: Router = Router();

menuRoutes.get("/menu", getMenus);
menuRoutes.post("/menu", addMenu);
menuRoutes.put("/menu/:id", updateMenu);
menuRoutes.delete("/menu/:id", deleteMenu);
menuRoutes.get("/menu/:id", retrieveMenu);

export default menuRoutes;
```

### Creating the server

First of all, let's add some env variables that will contain credentials for MongoDB database.

```json
// .nodemon.js
{
    "env": {
        "MONGO_USER": "your-username",
        "MONGO_PASSWORD": "your-password",
        "MONGO_DB": "your-db-name"
    }
}
```

You can get the credentials by creating a new cluster on  [MongoDB Atlas](https://www.mongodb.com/cloud/atlas).

> As those are database credentials, make sure to not push credentials on a repository or expose them.

```typescript
// .src/app.ts
import express from "express";
import mongoose from "mongoose";
import cors from "cors";
import menuRoutes from "./routes";

const app = express();

const PORT: string | number = process.env.PORT || 4000;

app.use(cors());
app.use(express.json());
app.use(menuRoutes);

const uri: string = `mongodb+srv://${process.env.MONGO_USER}:${process.env.MONGO_PASSWORD}@cluster0.raz9g.mongodb.net/${process.env.MONGO_DB}?retryWrites=true&w=majority`
mongoose
  .connect(uri)
  .then(() =>
    app.listen(PORT, () =>
      console.log(`Server running on http://localhost:${PORT}`)
    )
  )
  .catch((error) => {
    throw error;
  });
```
We first start by importing the `express` library to work with the `use` method to handles the Menus routes.

Next, we use the mongoose package to connect to MongoDB by appending to the URL the credentials held on the `nodemon.json` file.

Now, if the connection to the MongoDB database is successful the server will start otherwise an error will be throwing.

We've now done building the API with Node, Express, TypeScript, and MongoDB. 

To start your project, run `yarn start` and hit `http://localhost:4000`.

Here's are some tests you can make to the API using Postman or Insomnia.

> **Get All Menus** - `http://localhost:4000/menu`

```
GET http://localhost:4000/menu
```

> **Create a menu** - `http://localhost:4000/menu`

```
POST http://localhost:4000/menu
Content-Type: application/json

{
	"name": "Hot Dog",
	"description": "A hot dog",
	"price": 10
}
```

> **Update a menu** - `http://localhost:4000/menu/<menuId>`

```
PUT http://localhost:4000/menu/<menuId>
Content-Type: application/json

{
	"price": 5
}
```

Let's now dockerize the project.

## Docker + Docker Compose (Optional)

[Docker](https://www.docker.com/)  is an open platform for developing, shipping, and running applications inside containers. 
Why use Docker? 
It helps you separate your applications from your infrastructure and helps in delivering code faster. 

If it's your first time working with Docker, I highly recommend you go through a quick tutorial and read some documentation about it. 

Here are some great resources that helped me: 
-  [Docker Tutorial](https://www.youtube.com/watch?v=eN_O4zd4D9o&list=PLPoSdR46FgI5wOJuzcPQCNqS37t39zKkg) 
-  [Docker curriculum](https://docker-curriculum.com/) 

#### Dockerfile

The `Dockerfile` represents a text document containing all the commands that could call on the command line to create an image.

Add a Dockerfile to the project root:

```
FROM node:16-alpine

WORKDIR /app

COPY package.json ./

COPY yarn.lock ./

RUN yarn install --frozen-lockfile

COPY . .
```
Here, we started with an **Alpine-based Docker Image for Node**. It's a lightweight Linux distribution designed for security and resource efficiency. 

After that, we perform operations like: 
- Setting up work variables
- Copying there `package.json` and `yarn.lock` file to our app path
- Installing the project dependencies
- And last copying the entire project

Also, let's add a `.dockerignore` file. 

```
.dockerignore
Dockerfile
node_modules
```

Once it's done, we can now add docker-compose.

[Docker Compose](https://docs.docker.com/compose/)  is a great tool (<3). You can use it to define and run multi-container Docker applications.
 
What do we need? Well, just a YAML file containing all the configuration of our application's services. 
Then, with the `docker-compose` command, we can create and start all those services.

```yaml
version: '3.8'
services:
  api:
    container_name: node_api
    restart: on-failure
    build: .
    volumes:
      - ./src:/app/src
    ports:
      - "4000:4000"
    command: >
      sh -c "yarn start"
```

The setup is completed. Let's build our containers and test if everything works locally.

```
docker-compose up -d --build
```

Your project will be running on `https://localhost:4000/`.

## Conclusion

In this article,  we've learned how to build an API using NodeJS, TypeScript, Express, MongoDB, and Docker.

And as every article can be made better so your suggestion or questions are welcome in the comment section. 😉

Check the code of this tutorial  [here](https://github.com/koladev32/node-docker-tutorial).