Building & Consuming A RESTful CRUD API with Next.js, Tailwind, Node.js & Express: A Step-by-Step Guide (Part 1)
Published: 2023-10-30 17:57:29

In the constantly changing realm of web development, creating robust APIs is essential for seamless communication between applications. Representational State Transfer (REST) APIs, known for their simplicity and adaptability, have emerged as a universal preference among developers globally. In this guide, we will go through the process of building and consuming a RESTful CRUD API using the popular Node.js runtime environment and the Express.js framework by building a **Goal Tracker**.

---

## Understanding CRUD

Before we get started, let's understand what a RESTful CRUD API is. CRUD stands for `Create`, `Read`, `Update`, and `Delete`. It's an acronym used in computer programming to describe the four basic functions of a persistent storage system, like a database. These operations allow users to interact with data by creating new records, reading existing records, updating existing records, and deleting records that are no longer needed.

CRUD operations are fundamental in web development because they enable applications to interact with and manage data effectively, providing a seamless experience for users.

---

## RESTful API Standards

| Routes          | Description              |
| :-------------- | :----------------------- |
| GET /goals      | Get goals                |
| GET /goals/1    | Get goal with id of 1    |
| POST /goals     | Add a goal               |
| PUT /goals/1    | Update goal with id of 1 |
| DELETE /goals/1 | Delete goal with id of 1 |

---

## Setting Up Your Development Environment

Before we start building our API, we first need to set up our development environment.

1. Create a new directory:

	```sh
	mkdir goal-tracker
	```

2. Navigate to your directory that you just created with the following:

	```sh
	cd goal-tracker
	```

3. Initialize your Node.js project:

	```sh
	npm init -y
	```

---

## Setting Up Our Server

### Installing Express & Environment Variables

1. Install `express` & `dotenv`

	```sh
	npm install express dotenv
	```

&nbsp;

### Installing Nodemon

**Nodemon** is used to automatically restart the node application when a file changes.

1. Install `nodemon`

	```sh
	npm install -D nodemon
	```

&nbsp;

### Installing Colors

 1. Install `colors` (Optional - This just allows us to be able to distinguish between different messages within our console)

	```sh
	npm install colors
	```

2. Import `colors` in `server.js` to be able to use it

	```js
	import colors from 'colors';
	```

&nbsp;

### Updating Our Scripts  

1. Update your scripts in the `package.json` to include the following:
	
	```json
	{
	  "name": "goal-tracker",
	  "version": "1.0.0",
	  "description": "A goal tracking application to keep track of your goals",
	  "main": "server.js",
	  "scripts": {
	    "start": "SET NODE_ENV=production & node server",
	    "dev": "nodemon server"
	  },
	  "keywords": [],
	  "author": "Dylan Sleith",
	  "license": "ISC",
	}
	```

&nbsp;

### Adding Our Environment Variables

1. Create a file on the root of your project called `.env` and add the following:

	```env
	NODE_ENV=development
	PORT=5000
	```

&nbsp;

### Creating Our CRUD Routes

1. First let's allow us to be able to use **ES Modules** in Node.js by adding the following into our `package.json`

	```json
	{
	  // ...
	  "type": "module"
	}
	```

2. Let's add a basic setup for our routes. As a boilerplate, we can use the following code to set up our server with different routes depending on our CRUD operations. We will be refactoring this later on!

	```javascript
	import express from 'express';
	import dotenv from 'dotenv';
	import colors from 'colors';
	
	// Load environment variables
	dotenv.config();
	
	// Initialize Express
	const app = express();
	
	// CRUD Routes
	app.get('/api/v1/goals', (req, res) => {
	    res.status(200).json({ success: true, msg: 'Show all goals'});
	});
	
	app.get('/api/v1/goals/:id', (req, res) => {
	    res.status(200).json({ success: true, msg: `Get goal ${req.params.id}` });
	});
	
	app.post('/api/v1/goals', (req, res) => {
	    res.status(200).json({ success: true, msg: 'Create new goal'});
	});
	
	app.put('/api/v1/goals/:id', (req, res) => {
		res.status(200).json({ success: true, msg: `Update goal ${req.params.id}` });
	});
	
	app.delete('/api/v1/goals/:id', (req, res) => {
	    res.status(200).json({ success: true, msg: `Delete goal ${req.params.id}` });
	});
	
	// Port
	const PORT = process.env.PORT || 5000;
	
	// Listen for requests
	app.listen(
	  PORT,
	  console.log(
	    `Server is running in ${process.env.NODE_ENV} mode on port ${PORT}`.yellow
	      .bold
	  )
	);
	```

3. Now, let's separate our routes into its own folder so everything is not in the main `server.js` file. Create a new file in `/routes` called `goals.js` and move the routes from `server.js` to the new file, `goals.js`.

	```javascript
	import express from 'express';
	const router = express.Router();
	
	// CRUD Routes
	router.get('/', (req, res) => {
		res.status(200).json({ success: true, msg: 'Show all goals' });
	});
	
	router.get('/:id', (req, res) => {
		res.status(200).json({ success: true, msg: `Get goal ${req.params.id}` });
	});
	
	router.post('/', (req, res) => {
		res.status(200).json({ success: true, msg: 'Create new goal'});
	});
	
	router.put('/:id', (req, res) => {
		res.status(200).json({ success: true, msg: `Update goal ${req.params.id}`});
	});
	
	router.delete('/:id', (req, res) => {
		res.status(200).json({ success: true, msg: `Delete goal ${req.params.id}`});
	});
	
	export default router;
	```

4. Now, update `server.js` with the following:

	```javascript
	import express from 'express';
	import dotenv from 'dotenv';
	import colors from 'colors';
	import goals from './routes/goals.js';
	
	// Load environment variables
	dotenv.config();
	
	// Initialize Express
	const app = express();
	
	// Mount routers
	app.use('/api/v1/goals', goals); // "/api/v1/goals" => "/"
	
	// Port
	const PORT = process.env.PORT || 5000;
	
	// Listen for requests
	app.listen(
	  PORT,
	  console.log(
	    `Server is running in ${process.env.NODE_ENV} mode on port ${PORT}`.yellow
	      .bold
	  )
	);
	```

---

## Creating Our Controllers

Controllers are responsible for handling incoming requests and returning responses to the client.

1. Let's create our controllers. Create a new file in `/controllers` called `goals.js` and add the following controllers:

	```javascript
	// Goals CRUD Routes
	
	// @desc Get all goals
	// @route GET /api/v1/goals
	// @access Public
	export const getGoals = (req, res, next) => {
	  res.status(200).json({ success: true, msg: 'Show all goals' });
	};
	
	// @desc Get single goal
	// @route GET /api/v1/goals/:id
	// @access Public
	export const getGoal = (req, res, next) => {
	  res.status(200).json({ success: true, msg: `Get goal ${req.params.id}` });
	};
	
	// @desc Create new goal
	// @route POST /api/v1/goals
	// @access Private
	export const createGoal = (req, res, next) => {
	  res.status(200).json({ success: true, msg: 'Create new goal' });
	};
	
	// @desc Update goal
	// @route PUT /api/v1/goals/:id
	// @access Private
	export const updateGoal = (req, res, next) => {
	  res.status(200).json({ success: true, msg: `Update goal ${req.params.id}` });
	};
	
	// @desc Delete goal
	// @route DELETE /api/v1/goals/:id
	// @access Private
	export const deleteGoal = (req, res, next) => {
	  res.status(200).json({ success: true, msg: `Delete goal ${req.params.id}` });
	};
	```

2. Now we need to update our `routes.js` file to the following:

	```javascript
	import express from 'express';
	const router = express.Router();
	
	// Import Controllers
	import {
	  getGoals,
	  getGoal,
	  createGoal,
	  updateGoal,
	  deleteGoal,
	} from '../controllers/goals.js';
	
	// Attach Controllers to Routes
	router.route('/').get(getGoals).post(createGoal);
	
	router.route('/:id').get(getGoal).put(updateGoal).delete(deleteGoal);
	
	// Export router
	export default router;
	```

---

## Setting Up & Connecting To Our Database

### Creating Your Project

1. Go to [https://account.mongodb.com/account/login](https://account.mongodb.com/account/login) and log in
   
2. Click **Projects** > **New Project**
   
3. Enter your project name, in this case we will use **GoalTracker**, then click **Next**
   
4. Now click **Create Project**

&nbsp;

### Creating Your Project

1. Make sure you are on the **Overview** tab, then click **Create**
   
2. Use the following to setup your **Cluster**
	1. **Cluster:** M0
	2. **Provider:** AWS
	3. **Region:** us-east-1
	4. **Name:** (Your Cluster Name)
	   
3. Now click **Create**

&nbsp;

### Creating A Database User

1. Make sure you are on the **Database Access** tab, then click **Add New Database User**
   
2. Use the following to create your **Database User**
	1. **Authentication Method:** Password
	2. **Password Authentication:** 
		1. **Username:** (Your Username)
		2. **Password:** (Your Password)
	3. **Database User Privileges:** Read and write to any database
	   
3. Now click **Add User**

&nbsp;

### Whitelisting Our IP Address

1. Make sure you are on the **Network Access** tab, then click **Add IP Address**
   
2. Click **Add Current IP Address** and it will auto populate the field for you with your IP address or just click **Allow Access From Anywhere** to allow access from any IP address
   
3. Now click **Confirm**

&nbsp;

### Connecting To Our Database Using MongoDB Compass

1. Make sure you are on the **Overview** tab, then click **Connect**
   
2. Under **Access your data through tools**, click **Compass**
   
3. Copy the **Connection String**, then open **MongoDB Compass**
   
4. In **MongoDB Compass**, under **New Connection**, paste the **Connection String** that you just copied
   
	```sh
	mongodb+srv://username:<password>@goaltracker.ofvhjnl.mongodb.net/
	```

5. Now click **Connect**

&nbsp;

### Connecting To Our Database Within Our Project

1. Install `mongoose`

	```sh
	npm install mongoose
	```

2. Add a new environment variable to your `.env` file:

	```env
	MONGO_URI=mongodb+srv://username:<password>@goaltracker.ofvhjnl.mongodb.net/goaltracker
	```

3. Create a new directory called `/lib` and then create a new file in that directory called `db.js` and add the following:

	```javascript
	import mongoose from 'mongoose';
	
	const connectDb = async () => {
	  const conn = await mongoose.connect(process.env.MONGO_URI, {
	    useNewUrlParser: true,
	    useUnifiedTopology: true,
	  });
	
	  console.log(`MongoDB Connected: ${conn.connection.host}`.cyan.underline.bold);
	};
	
	export default connectDb;
	```

4. Now in `server.js` we need to connect to our MongoDB Database, add `connectDb();` after you load your environment variables in `server.js`:

	```javascript
	import express from 'express';
	import dotenv from 'dotenv';
	import colors from 'colors';
	import goals from './routes/goals.js';
	import connectDb from './lib/db.js';
	
	// Load environment variables
	dotenv.config();
	
	// Connect to database
	connectDb();
	
	// Initialize Express
	const app = express();
	
	// ...
	```

---

## Creating Our Goal Model/Schema

We need to create our **Goal Model/Schema** in order to describe how are data is going to look like, there are also some other use cases such as data validation, security, etc.

1. Create a new directory called `/model` and then create a new file in that directory called `Goal.js` and add the following:

	```javascript
	import mongoose from 'mongoose';
	
	const GoalSchema = new mongoose.Schema({
	  name: {
	    type: String,
	    required: [true, 'Please add a goal name'],
	    unique: true,
	    trim: true,
	    maxLength: [20, 'Goal name can not be more than 20 characters'],
	  },
	  priority: {
	    type: String,
	    required: true,
	    enum: ['Low', 'Medium', 'High'],
	  },
	  status: {
	    type: String,
	    required: true,
	    enum: ['Pending', 'In Progress', 'Completed'],
	  },
	  createdAt: {
	    type: Date,
	    default: Date.now,
	  },
	  updatedAt: {
	    type: Date,
	    default: Date.now,
	  },
	});
	
	export default mongoose.model('Goal', GoalSchema);
	```

---

## Creating Our CRUD Operations

### POST (Create) - Create New Goal

#### Creating Our POST Request

1. Before we get started, we need to go into our `server.js` file and add the `Body Parser` middleware so that we can read data from the request.

	```javascript
	import express from 'express';
	import dotenv from 'dotenv';
	import colors from 'colors';
	import goals from './routes/goals.js';
	import connectDb from './lib/db.js';
	
	// ...
	
	// Body parser
	app.use(express.json());
	```

2. We need to add the following logic into our **createGoal** controller to have a working endpoint for our POST request:

	```javascript
	// @desc Create new goal
	// @route POST /api/v1/goals
	// @access Private
	export const createGoal = async (req, res, next) => {
	  try {
	    const goal = await Goal.create(req.body);
	
	    res.status(201).json({
	      success: true,
	      data: goal,
	    });
	  } catch (err) {
	    res.status(400).json({
	      success: false,
	    });
	  }
	};
	```

&nbsp;

#### Testing Our POST Request in Postman

1. Open **Postman**
   
2. Create a new HTTP Request
   
3. Add the following information to the request
	1. URL: `http://localhost:5000/api/v1/goals`
	2. Method: `POST`
	3. Headers: `'Content-Type': 'application/json'`
	4. Body (raw JSON):
	   
	```json
	{
	    "name": "Learn tRPC",
	    "priority": "Medium",
	    "status": "Completed"
	}
	```

4. Click **Send** and you will get a response of **201 Created** which means you have successfully created a new record and it will be created in your database.

&nbsp;

### GET (Read)  - Get All Goals

#### Creating Our GET Request

1. We need to add the following logic into our **getGoals** controller to have a working endpoint for our GET request:

	```javascript
	// @desc Get all goals
	// @route GET /api/v1/goals
	// @access Public
	export const getGoals = async (req, res, next) => {
	  try {
	    const goals = await Goal.find();
	
	    res.status(200).json({ success: true, count: goals.length, data: goals });
	  } catch (err) {
	    res.status(400).json({ success: false });
	  }
	};
	```

&nbsp;

#### Testing Our GET Request in Postman

1. Open **Postman**
   
2. Create a new HTTP Request
   
3. Add the following information to the request
	1. URL: `http://localhost:5000/api/v1/goals`
	2. Method: `GET`
	   
4. Click **Send** and you will get a response of **200 OK** which means everything went okay and you will get a list of goals displayed back to you.

&nbsp;

### GET (Read)  - Get Single Goal

#### Creating Our GET Request

1. We need to add the following logic into our **getGoal** controller to have a working endpoint for our GET request:

	```javascript
	// @desc Get single goal
	// @route GET /api/v1/goals/:id
	// @access Public
	export const getGoal = async (req, res, next) => {
	  try {
	    const goal = await Goal.findById(req.params.id);
	
	    if (!goal) {
	      return res.status(400).json({ success: false });
	    }
	
	    res.status(200).json({ success: true, data: goal });
	  } catch (error) {
	    res.status(400).json({ success: false });
	  }
	};
	```

&nbsp;

#### Testing Our GET Request in Postman

1. Open **Postman**
   
2. Create a new HTTP Request
   
3. Add the following information to the request
	1. URL: `http://localhost:5000/api/v1/goals/653fac8d0d988dfba8a35e76`
	2. Method: `GET`
	   
4. Click **Send** and you will get a response of **200 OK** which means everything went okay and you will get a single goal displayed back to you depending on the id you chose.

&nbsp;

### PUT (Update)  - Update Goal

#### Creating Our PUT Request

1. We need to add the following logic into our **updateGoal** controller to have a working endpoint for our PUT request:

	```javascript
	// @desc Update goal
	// @route PUT /api/v1/goals/:id
	// @access Private
	export const updateGoal = async (req, res, next) => {
	  try {
	    const goal = await Goal.findByIdAndUpdate(req.params.id, req.body, {
	      new: true,
	      runValidators: true,
	    });
	
	    if (!goal) {
	      return res.status(400).json({ success: false });
	    }
	
	    res.status(200).json({ success: true, data: goal });
	  } catch (err) {
	    res.status(400).json({
	      success: false,
	    });
	  }
	};
	```

&nbsp;

#### Testing Our PUT Request in Postman

1. Open **Postman**
   
2. Create a new HTTP Request
   
3. Add the following information to the request
	1. URL: `http://localhost:5000/api/v1/goals/653fafc50d988dfba8a35e7a`
	2. Method: `PUT`
	3. Headers: `'Content-Type': 'application/json'`
	4. Body (raw JSON):
	   
	```json
	{
	    "status": "In Progress"
	}
	```

4. Click **Send** and you will get a response of **200 OK** which means you have successfully updated an existing record and it will also be updated in your database.

&nbsp;

### DELETE (Delete)  - Delete Goal

#### Creating Our DELETE Request

1. We need to add the following logic into our **deleteGoal** controller to have a working endpoint for our DELETE request:

	```javascript
	// @desc Delete goal
	// @route DELETE /api/v1/goals/:id
	// @access Private
	export const deleteGoal = async (req, res, next) => {
	  try {
	    const goal = await Goal.findByIdAndDelete(req.params.id);
	
	    if (!goal) {
	      return res.status(400).json({ success: false });
	    }
	
	    res.status(200).json({ success: true, data: {} });
	  } catch (err) {
	    res.status(400).json({
	      success: false,
	    });
	  }
	};
	```

&nbsp;

#### Testing Our DELETE Request in Postman

1. Open **Postman**
   
2. Create a new HTTP Request
   
3. Add the following information to the request
	1. URL: `http://localhost:5000/api/v1/goals/653fac8d0d988dfba8a35e76`
	2. Method: `DELETE`
	   
4. Click **Send** and you will get a response of **200 OK**  which means you have successfully deleted a record from your database.

---

In summary, building a REST API with Node.js is a rewarding endeavour that requires understanding RESTful principles, error handling, and security measures. Leveraging tools like Express.js and staying updated with the latest trends is crucial. 

&nbsp;

### Thanks for reading!

Have a question? Connect with me via [Twitter](https://twitter.com/dylansleith) or send me a message at [hello@dylansleith.com](mailto:hello@dylansleith.com)