DEV Community

Dylan 💻
Dylan 💻

Posted on • Updated on

Building & Consuming A RESTful CRUD API with Next.js, Tailwind, Node.js & Express: A Step-by-Step Guide (Part 1)

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:

    mkdir goal-tracker
    
  2. Navigate to your directory that you just created with the following:

    cd goal-tracker
    
  3. Initialize your Node.js project:

    npm init -y
    

Setting Up Our Server

Installing Express & Environment Variables

  1. Install express & dotenv

    npm install express dotenv
    

 

Installing Nodemon

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

  1. Install nodemon

    npm install -D nodemon
    

 

Installing Colors

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

    npm install colors
    
    1. Import colors in server.js to be able to use it
    import colors from 'colors';
    

 

Updating Our Scripts

  1. Update your scripts in the package.json to include the following:

    {
      "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",
    }
    

 

Adding Our Environment Variables

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

    NODE_ENV=development
    PORT=5000
    

 

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

    {
      // ...
      "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!

    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.

    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:

    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:

    // 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:

    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 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

 

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

 

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

 

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

 

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

    mongodb+srv://username:<password>@goaltracker.ofvhjnl.mongodb.net/
    
  5. Now click Connect

 

Connecting To Our Database Within Our Project

  1. Install mongoose

    npm install mongoose
    
  2. Add a new environment variable to your .env file:

    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:

    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:

    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:

    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.

    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:

    // @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,
        });
      }
    };
    

 

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):
    {
        "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.

 

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:

    // @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 });
      }
    };
    

 

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.

 

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:

    // @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 });
      }
    };
    

 

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.

 

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:

    // @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,
        });
      }
    };
    

 

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):
    {
        "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.

 

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:

    // @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,
        });
      }
    };
    

 

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.

 

Thanks for reading!

Have a question? Connect with me via Twitter or send me a message at hello@dylansleith.com

Top comments (1)

Collapse
 
dotenv profile image
Dotenv

💛🌴