Build a Restful CRUD API with Node.js
Tags: node, javascript, webdev, tutorial
Published: 2021-10-07 19:46:13

## Table of Contents
1. [What CRUD API means](#what-crud-api-means)
2. [Let's Start](#lets-start)


## What CRUD API means?

The CRUD paradigm stands for the four primitive database operations that  are `CREATE`, `READ`, `UPDATE` and `DELETE`.

So, with the term `CRUD API` we mean, the API which have the ability to `create`, `read`, `update` and `delete` entities from a database. For this example, the entity is the employee.

## Let's Start

API Endpoints are the following

| Methods     | Urls             |Description            |
| ----------- | -----------      | -----------        |
| GET         | api/employees    |Get all employees           |
| GET         | api/employees/id |Get a specific employee         |
| POST        | api/employees    |Create a new employee         |
| PUT        | api/employees/id    |Update an existing employee|
| DELETE        | api/employees/id    |Delete an existing employee|

**We create the repository and install the dependencies.**
*The entry point is the server.js file.*

```bash
 mkdir express-api
 cd express-api
 npm init
 npm install express helmet morgan body-parser monk joi dotenv --save 
 npm install nodemon --save-dev
```
*About the packages*

**express:** It is a minimal and flexible Node.js web application framework.
**helmet:** It helps in securing HTTP headers in express applications.
**morgan:** It is an HTTP request logger middleware for Node. js
**body-parser:** It is responsible for parsing the incoming request bodies.
**monk:** A tiny layer that provides substantial usability improvements for MongoDB usage.
**joi:** It is an object schema description language and object validator.
**dotenv:** It loads environment variables from a .env file.
**nodemon:** It restarts automatically the node application when file changes in the directory have been detected.


**Setup Express Web Server**
./src/server.js

```javascript
const express = require('express');
const morgan = require('morgan');
const helmet = require('helmet');
const bodyParser = require('body-parser');

require('dotenv').config();

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

app.use(helmet());
app.use(morgan('dev'));
app.use(bodyParser.json());

const port = process.env.PORT || 8080;
app.listen(port, () => {
  console.log(`Listening on port ${port}`);
});
```
**Create and configure the .env file**
./.env
*It contains all the enviroment variables that we use.
`TEST_DB_URL` variable is for test cases in order to prevent the insertion of test data in the database. Also, you can specify the port number you want.*

```
DB_URL = localhost/my-employees
TEST_DB_URL = localhost/test-my-employees
PORT = 5000
```

**./src/db/schema.js**
*Create the data schema and define the validation rules that the properties `name` and `job` have to follow.*
```javascript
const Joi = require('joi');

const schema = Joi.object({
    name: Joi.string()
        .min(3)
        .max(30)
        .required(),
    job: Joi.string()
        .min(3)
        .max(30)
        .required(),
})

module.exports = schema;
```

**./src/db/connection.js**
*Connect to the database*
```javascript
const monk = require('monk');

let dbUrl = process.env.DB_URL;

if (process.env.NODE_ENV === 'test') {
  dbUrl = process.env.TEST_DB_URL;
}

const db = monk(dbUrl);

module.exports = db;
```

**./src/middlewares/index.js**
*Create the error middleware to handle the errors and give properly responses.*

```javascript
function notFound(req, res, next) {
    res.status(404);
    const error = new Error('Not Found', req.originalUrl);
    next(error);
}

function errorHandler(err, req, res, next){
    res.status(res.statusCode || 500);
    res.json({
        message: err.message,
        stack: err.stack
    });
}

module.exports = {
    notFound,
    errorHandler
}
```
We import `./src/db/connection.js`, `./src/db/schema.js` and `./src/middlewares/index.js` files in `./src/server.js`

```javascript
const express = require('express');
const morgan = require('morgan');
const helmet = require('helmet');
const bodyParser = require('body-parser');

const { notFound, errorHandler } = require('./middlewares');

require('dotenv').config();

const schema = require('./db/schema');
const db = require('./db/connection');
const employees = db.get('employees');

const app = express();

app.use(helmet());
app.use(morgan('dev'));
app.use(bodyParser.json());

app.use(notFound);
app.use(errorHandler);

const port = process.env.PORT || 8080;
app.listen(port, () => {
  console.log(`Listening on port ${port}`);
});
```

**Now, we code the API Endpoints**

```javascript
const express = require('express');
const morgan = require('morgan');
const helmet = require('helmet');
const bodyParser = require('body-parser');

const { notFound, errorHandler } = require('./middlewares');

require('dotenv').config();

const schema = require('./db/schema');
const db = require('./db/connection');
const employees = db.get('employees');

const app = express();

app.use(helmet());
app.use(morgan('dev'));
app.use(bodyParser.json());

/* Get all employees */
app.get('/', async (req, res, next) => {
    try {
        const allEmployees = await employees.find({});
        res.json(allEmployees);
    } catch(error) {
        next(error);
    }
});

/* Get a specific employee */
app.get('/:id', async (req, res, next) => {
    try {
        const { id } = req.params;
        const employee = await employees.findOne({
            _id: id
        });

        if(!employee) {
            const error = new Error('Employee does not exist');
            return next(error);
        }

    res.json(employee);
    } catch(error) {
        next(error);
    }
});

/* Create a new employee */
app.post('/', async (req, res, next) => {
    try {
        const { name, job } = req.body;
        const result = await schema.validateAsync({ name, job });
       
        const employee = await employees.findOne({
            name,
        })

        // Employee already exists
        if (employee) {
            res.status(409); // conflict error
            const error = new Error('Employee already exists');
            return next(error);
        } 

        const newuser = await employees.insert({
            name,
            job,
        });

        console.log('New employee has been created');
        res.status(201).json(newuser);
    } catch(error) {
        next(error);
    }
});

/* Update a specific employee */
app.put('/:id', async (req, res, next) => {
    try {
        const { id } = req.params;
        const { name, job } = req.body;
        const result = await schema.validateAsync({ name, job });
        const employee = await employees.findOne({
            _id: id
        });

        // Employee does not exist
        if(!employee) {
            return next();
        }

        const updatedEmployee = await employees.update({
            _id: id,
            }, {  
            $set: result},
            { upsert: true }
        );

        res.json(updatedEmployee);
    } catch(error) {
        next(error);
    }
});

/* Delete a specific employee */
app.delete('/:id', async (req, res, next) => {
    try {
        const { id } = req.params;
        const employee = await employees.findOne({
            _id: id
        });

        // Employee does not exist
        if(!employee) {
            return next();
        }
        await employees.remove({
            _id: id
        });

        res.json({
            message: 'Success'
        });

    } catch(error) {
        next(error);
    }
});

app.use(notFound);
app.use(errorHandler);

const port = process.env.PORT || 8080;
app.listen(port, () => {
  console.log(`Listening on port ${port}`);
});
```
We go to the `package.json` file and replace the script section with the following

```json
"scripts": {
    "start": "node src/server.js",
    "dev": "nodemon src/server.js"
  },
```
The command `npm start` starts the Node.js application and the command `npm run dev` starts the Node.js application with the only difference that any change we do, will automatically be monitored by nodemon.

We "split" the `./src/server.js` and create the `./src/app.js` file.

**./src/app.js**
```javascript
const express = require('express');
const morgan = require('morgan');
const helmet = require('helmet');
const bodyParser = require('body-parser');

const { notFound, errorHandler } = require('./middlewares');

require('dotenv').config();

const schema = require('./db/schema');
const db = require('./db/connection');
const employees = db.get('employees');

const app = express();

app.use(helmet());
app.use(morgan('dev'));
app.use(bodyParser.json());

/* Get all employees */
app.get('/', async (req, res, next) => {
    try {
        const allEmployees = await employees.find({});
        res.json(allEmployees);
    } catch(error) {
        next(error);
    }
});

/* Get a specific employee */
app.get('/:id', async (req, res, next) => {
    try {
        const { id } = req.params;
        const employee = await employees.findOne({
            _id: id
        });

        if(!employee) {
            const error = new Error('Employee does not exist');
            return next(error);
        }

    res.json(employee);
    } catch(error) {
        next(error);
    }
});

/* Create a new employee */
app.post('/', async (req, res, next) => {
    try {
        const { name, job } = req.body;
        const result = await schema.validateAsync({ name, job });
       
        const employee = await employees.findOne({
            name,
        })

        // Employee already exists
        if (employee) {
            res.status(409); // conflict error
            const error = new Error('Employee already exists');
            return next(error);
        } 

        const newuser = await employees.insert({
            name,
            job,
        });

        console.log('New employee has been created');
        res.status(201).json(newuser);
    } catch(error) {
        next(error);
    }
});

/* Update a specific employee */
app.put('/:id', async (req, res, next) => {
    try {
        const { id } = req.params;
        const { name, job } = req.body;
        const result = await schema.validateAsync({ name, job });
        const employee = await employees.findOne({
            _id: id
        });

        // Employee does not exist
        if(!employee) {
            return next();
        }

        const updatedEmployee = await employees.update({
            _id: id,
            }, {  
            $set: result},
            { upsert: true }
        );

        res.json(updatedEmployee);
    } catch(error) {
        next(error);
    }
});

/* Delete a specific employee */
app.delete('/:id', async (req, res, next) => {
    try {
        const { id } = req.params;
        const employee = await employees.findOne({
            _id: id
        });

        // Employee does not exist
        if(!employee) {
            return next();
        }
        await employees.remove({
            _id: id
        });

        res.json({
            message: 'Success'
        });

    } catch(error) {
        next(error);
    }
});

app.use(notFound);
app.use(errorHandler);

module.exports = app;
```

**./src/server.js**
```javascript
const app = require('./app');

const port = process.env.PORT || 8080;
app.listen(port, () => {
  console.log(`Listening on port ${port}`);
});
```

Last step is to refactor our code and create `./src/routes/employees`.

**./src/routes/employees.js**

```javascript
const express = require('express');
const schema = require('../db/schema');
const db = require('../db/connection');

const employees = db.get('employees');

const router = express.Router();

/* Get all employees */
router.get('/', async (req, res, next) => {
  try {
    const allEmployees = await employees.find({});
    res.json(allEmployees);
  } catch (error) {
    next(error);
  }
});

/* Get a specific employee */
router.get('/:id', async (req, res, next) => {
  try {
    const { id } = req.params;
    const employee = await employees.findOne({
      _id: id,
    });

    if (!employee) {
      const error = new Error('Employee does not exist');
      return next(error);
    }

    res.json(employee);
  } catch (error) {
    next(error);
  }
});

/* Create a new employee */
router.post('/', async (req, res, next) => {
  try {
    const { name, job } = req.body;
    const result = await schema.validateAsync({ name, job });

    const employee = await employees.findOne({
      name,
    });

    // Employee already exists
    if (employee) {
      const error = new Error('Employee already exists');
      res.status(409); // conflict error
      return next(error);
    }

    const newuser = await employees.insert({
        name,
        job,
    });

    res.status(201).json(newuser);
  } catch (error) {
    next(error);
  }
});

/* Update a specific employee */
router.put('/:id', async (req, res, next) => {
  try {
    const { id } = req.params;
    const { name, job } = req.body;
    const result = await schema.validateAsync({ name, job });
    const employee = await employees.findOne({
      _id: id,
    });

    // Employee does not exist
    if (!employee) {
      return next();
    }

    const updatedEmployee = await employees.update({
      _id: id,
    }, { $set: result },
    { upsert: true });

    res.json(updatedEmployee);
  } catch (error) {
    next(error);
  }
});

/* Delete a specific employee */
router.delete('/:id', async (req, res, next) => {
  try {
    const { id } = req.params;
    const employee = await employees.findOne({
      _id: id,
    });

    // Employee does not exist
    if (!employee) {
      return next();
    }
    await employees.remove({
      _id: id,
    });

    res.json({
      message: 'Employee has been deleted',
    });
  } catch (error) {
    next(error);
  }
});

module.exports = router;
```

and the **./src/app.js** file looks like this

```javascript
const express = require('express');
const morgan = require('morgan');
const helmet = require('helmet');
const bodyParser = require('body-parser');

const { notFound, errorHandler } = require('./middlewares');

const app = express();

require('dotenv').config();

app.use(helmet());
app.use(morgan('dev'));
app.use(bodyParser.json());

const employees = require('./routes/employees');

app.use('/api/employees', employees);

app.use(notFound);
app.use(errorHandler);

module.exports = app;

```

You can check the whole project in my github repository  [express-api](https://github.com/zagaris/express-api)