How to Build and Secure a REST API in Node.js with Examples?

HomeStaff augmentationHow to Build and Secure a REST API in Node.js with Examples?

Share

Key Takeaways

REST APIs enable seamless client-server communication, and Node.js is ideal for fast, scalable APIs.

A clean project structure (routes, controllers, models, middleware, config) ensures maintainability.

Essential Node.js packages for REST APIs: Express, Mongoose, dotenv, CORS, Helmet, rate-limit, Nodemon.

Security best practices include HTTPS, JWT/OAuth, input validation, CORS, rate limiting, and headers via Helmet.

Testing with Postman, Jest, Mocha/Chai, and CI/CD pipelines ensures reliability and reduces production errors.

REST APIs have become the backbone of modern web applications, enabling seamless communication between clients and servers. Node.js, with its event-driven architecture and non-blocking I/O model, provides an excellent platform for building fast and scalable REST APIs. In this guide, we’ll walk through the complete process of creating a secure REST API using Node.js, Express, and MongoDB, complete with practical examples you can implement right away.

Set up the node.js project

Before diving into code, we need to set up our project environment. A well-structured project makes development and maintenance much easier in the long run.

Install required packages

First, create a new directory for your project and initialize it:

mkdir node-rest-api

cd node-rest-api

npm init -y

Next, install the core packages needed for our REST API:

npm install express mongoose dotenv

npm install nodemon cors helmet express-rate-limit --save-dev

These packages serve specific purposes:

  • express: Web framework for creating server and routes
  • mongoose: MongoDB object modeling tool
  • dotenv: Loads environment variables from .env files
  • cors: Enables Cross-Origin Resource Sharing
  • helmet: Secures Express apps by setting various HTTP headers
  • express-rate-limit: Basic rate-limiting middleware
  • nodemon: Automatically restarts the server during development

Set up entry point file

Create a file named server.js in your project root. This will be our application’s entry point:

// server.js

require('dotenv').config();

const express = require('express');

const mongoose = require('mongoose');

const cors = require('cors');

const helmet = require('helmet');

const rateLimit = require('express-rate-limit');

const app = express();

const PORT = process.env.PORT || 3000;

// Middleware setup

app.use(express.json());

app.use(cors());

app.use(helmet());

// Apply rate limiting

const limiter = rateLimit({

  windowMs: 15 * 60 * 1000, // 15 minutes

  max: 100 // limit each IP to 100 requests per windowMs

});

app.use(limiter);

// Root route

app.get('/', (req, res) => {

  res.send('Welcome to our REST API in Node.js');

});

// Start server

app.listen(PORT, () => {

  console.log(`Server running on port ${PORT}`);

});

Define folder structure

Organize your project with a clear folder structure to make it scalable and maintainable:

node-rest-api/

├── config/         # Configuration files

├── controllers/    # Route controllers

├── middleware/     # Custom middleware

├── models/         # Mongoose models

├── routes/         # API routes

├── utils/          # Utility functions

├── .env            # Environment variables

├── .gitignore      # Git ignore file

├── package.json    # Project dependencies

└── server.js       # Entry point

Create these folders to prepare for the next steps:

mkdir config controllers middleware models routes utils

Create the REST API in Node.js

Now that our project structure is ready, let’s build the actual REST API functionality.

Set up Express server

Our server.js file already contains the basic Express setup. Let’s enhance it by creating a configuration file for database connections.

Create config/db.js:

Staff Augmentation Service

Tap Into a Talent Ecosystem Powered by 1500+ Agencies and 1,900+ Projects. EMB’s Staff Augmentation Services Help You Stay Agile, Competitive, and Fully Resourced.

Get Quote
// config/db.js

const mongoose = require('mongoose');

const connectDB = async () => {

  try {

    const conn = await mongoose.connect(process.env.MONGO_URI, {

      useNewUrlParser: true,

      useUnifiedTopology: true

    });

    console.log(`MongoDB Connected: ${conn.connection.host}`);

  } catch (err) {

    console.error(`Error: ${err.message}`);

    process.exit(1);

  }

};

module.exports = connectDB;

Now, update server.js to use this connection:

// Add at the top of server.js after require statements

const connectDB = require('./config/db');

// Connect to database

connectDB();

// Rest of the code remains the same

Define CRUD API routes

Let’s create routes for a simple resource, such as products. First, create the model:

// models/Product.js

const mongoose = require('mongoose');

const ProductSchema = new mongoose.Schema({

  name: {

    type: String,

    required: [true, 'Please add a name'],

    trim: true,

    maxlength: [50, 'Name cannot be more than 50 characters']

  },

  description: {

    type: String,

    required: [true, 'Please add a description'],

    maxlength: [500, 'Description cannot be more than 500 characters']

  },

  price: {

    type: Number,

    required: [true, 'Please add a price']

  },

  createdAt: {

    type: Date,

    default: Date.now

  }

});

module.exports = mongoose.model('Product', ProductSchema);

Next, create a controller to handle product operations:

// controllers/productController.js

const Product = require('../models/Product');

// Get all products

exports.getProducts = async (req, res) => {

  try {

    const products = await Product.find();

    res.status(200).json({ success: true, data: products });

  } catch (error) {

    res.status(400).json({ success: false, error: error.message });

  }

};

// Get single product

exports.getProduct = async (req, res) => {

  try {

    const product = await Product.findById(req.params.id);

    if (!product) {

      return res.status(404).json({ success: false, error: 'Product not found' });

    }

    res.status(200).json({ success: true, data: product });

  } catch (error) {

    res.status(400).json({ success: false, error: error.message });

  }

};

// Create new product

exports.createProduct = async (req, res) => {

  try {

    const product = await Product.create(req.body);

    res.status(201).json({ success: true, data: product });

  } catch (error) {

    res.status(400).json({ success: false, error: error.message });

  }

};

// Update product

exports.updateProduct = async (req, res) => {

  try {

    const product = await Product.findByIdAndUpdate(req.params.id, req.body, {

      new: true,

      runValidators: true

    });

    if (!product) {

      return res.status(404).json({ success: false, error: 'Product not found' });

    }

    res.status(200).json({ success: true, data: product });

  } catch (error) {

    res.status(400).json({ success: false, error: error.message });

  }

};

// Delete product

exports.deleteProduct = async (req, res) => {

  try {

    const product = await Product.findByIdAndDelete(req.params.id);

    if (!product) {

      return res.status(404).json({ success: false, error: 'Product not found' });

    }

    res.status(200).json({ success: true, data: {} });

  } catch (error) {

    res.status(400).json({ success: false, error: error.message });

  }

};

Use middleware for routing

Now, let’s create the routes and use Express Router as middleware:

// routes/productRoutes.js

const express = require('express');

const router = express.Router();

const {

  getProducts,

  getProduct,

  createProduct,

  updateProduct,

  deleteProduct

} = require('../controllers/productController');

router.route('/')

  .get(getProducts)

  .post(createProduct);

router.route('/:id')

  .get(getProduct)

  .put(updateProduct)

  .delete(deleteProduct);

module.exports = router;

Finally, update server.js to use these routes:

// Add after middleware setup in server.js

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

// Mount routes

app.use('/api/products', productRoutes);

Add database support with MongoDB

We’ve already set up our database connection and created a model. Now let’s look at more advanced database operations.

Connect MongoDB using Mongoose

For a production application, we need to handle database connection errors and implement retry logic. Let’s update our config/db.js file:

// config/db.js (enhanced version)

const mongoose = require('mongoose');

const connectDB = async () => {

  try {

    const conn = await mongoose.connect(process.env.MONGO_URI, {

      useNewUrlParser: true,

      useUnifiedTopology: true,

      serverSelectionTimeoutMS: 5000

    });

    console.log(`MongoDB Connected: ${conn.connection.host}`);

    mongoose.connection.on('error', err => {

      console.error(`MongoDB connection error: ${err}`);

    });

    mongoose.connection.on('disconnected', () => {

      console.log('MongoDB disconnected, trying to reconnect...');

      setTimeout(connectDB, 5000);

    });

  } catch (err) {

    console.error(`Error: ${err.message}`);

    // Attempt to reconnect after 5 seconds

    console.log('Attempting to reconnect to MongoDB...');

    setTimeout(connectDB, 5000);

  }

};

module.exports = connectDB;

Create a .env file in the root directory to store your MongoDB connection string:

PORT=3000

MONGO_URI=mongodb://localhost:27017/node-rest-api

NODE_ENV=development

Create and use data models

Let’s enhance our product model with more advanced features:

// models/Product.js (enhanced)

const mongoose = require('mongoose');

const slugify = require('slugify');

const ProductSchema = new mongoose.Schema({

  name: {

    type: String,

    required: [true, 'Please add a name'],

    unique: true,

    trim: true,

    maxlength: [50, 'Name cannot be more than 50 characters']

  },

  slug: String,

  description: {

    type: String,

    required: [true, 'Please add a description'],

    maxlength: [500, 'Description cannot be more than 500 characters']

  },

  price: {

    type: Number,

    required: [true, 'Please add a price']

  },

  category: {

    type: String,

    required: [true, 'Please add a category'],

    enum: ['Electronics', 'Books', 'Clothing', 'Food']

  },

  stock: {

    type: Number,

    required: [true, 'Please add stock quantity'],

    min: [0, 'Stock cannot be negative']

  },

  createdAt: {

    type: Date,

    default: Date.now

  }

}, {

  toJSON: { virtuals: true },

  toObject: { virtuals: true }

});

// Create product slug from name

ProductSchema.pre('save', function(next) {

  this.slug = slugify(this.name, { lower: true });

  next();

});

// Virtual for formatted price

ProductSchema.virtual('formattedPrice').get(function() {

  return `$${this.price.toFixed(2)}`;

});

module.exports = mongoose.model('Product', ProductSchema);

Remember to install the slugify package:

npm install slugify

Handle data from API endpoints

Let’s improve our controller to handle filtering, sorting, and pagination:

// controllers/productController.js (enhanced getProducts method)

exports.getProducts = async (req, res) => {

  try {

    // Copy req.query

    const reqQuery = { ...req.query };

    // Fields to exclude

    const removeFields = ['select', 'sort', 'page', 'limit'];

    // Loop over removeFields and delete them from reqQuery

    removeFields.forEach(param => delete reqQuery[param]);

    // Create query string

    let queryStr = JSON.stringify(reqQuery);

    // Create operators ($gt, $gte, etc)

    queryStr = queryStr.replace(/\b(gt|gte|lt|lte|in)\b/g, match => `$${match}`);

    // Finding resource

    let query = Product.find(JSON.parse(queryStr));

    // Select Fields

    if (req.query.select) {

      const fields = req.query.select.split(',').join(' ');

      query = query.select(fields);

    }

    // Sort

    if (req.query.sort) {

      const sortBy = req.query.sort.split(',').join(' ');

      query = query.sort(sortBy);

    } else {

      query = query.sort('-createdAt');

    }

    // Pagination

    const page = parseInt(req.query.page, 10) || 1;

    const limit = parseInt(req.query.limit, 10) || 25;

    const startIndex = (page - 1) * limit;

    const endIndex = page * limit;

    const total = await Product.countDocuments();

    query = query.skip(startIndex).limit(limit);

    // Executing query

    const products = await query;

    // Pagination result

    const pagination = {};

    if (endIndex < total) {

      pagination.next = {

        page: page + 1,

        limit

      };

    }

    if (startIndex > 0) {

      pagination.prev = {

        page: page - 1,

        limit

      };

    }

    res.status(200).json({

      success: true,

      count: products.length,

      pagination,

      data: products

    });

  } catch (error) {

    res.status(400).json({ success: false, error: error.message });

  }

};

Wrap up your secure Rest API in Node.js project

We’ve built a robust REST API in Node.js with Express and MongoDB. Our API includes proper project structure, CRUD operations, data validation, error handling, and advanced features like filtering, sorting, and pagination. This foundation can be extended for various applications requiring secure server-client communication.

To further enhance your API, consider adding:

  • JWT authentication
  • Role-based access control
  • API documentation with Swagger
  • Comprehensive testing
  • Logging and monitoring

With the skills learned here, you can create powerful REST APIs to support web and mobile applications, microservices, or any system requiring HTTP-based communication.

FAQ

Q1. How do I create a secure REST API in Node.js?

Creating a secure REST API in Node.js involves multiple layers of protection. Start by using HTTPS, implementing proper authentication (JWT, OAuth), validating all inputs, using security packages like Helmet, implementing rate limiting, sanitizing database queries, encrypting sensitive data, and keeping dependencies updated. Adding CORS protection and security headers also significantly improves your API’s security posture.

Q2. What is a real-world REST API example in Node.js?

A common real-world example is an e-commerce API that handles products, users, and orders. It would include endpoints for user registration/authentication, product browsing with filtering and searching, shopping cart management, checkout processing, and order history. Such an API would connect to payment gateways, handle inventory updates, and possibly integrate with shipping services, while maintaining proper authentication and authorization throughout.

Q3. How can I test my Node.js REST API easily?

Testing a REST API can be done at multiple levels. For quick manual testing, use tools like Postman or Insomnia. For automated testing, use frameworks like Mocha, Chai, and Supertest for unit and integration tests. Jest is also popular. Write tests for each endpoint, checking successful operations and error handling. Mock external services and databases using tools like Sinon.js. Finally, set up a CI/CD pipeline to run tests automatically before deployment.

Q4. What is the best way to organize REST APIs in Node?

The best organization follows the MVC pattern with some modifications. Structure your project with clear separation of concerns: routes (defining endpoints), controllers (handling request/response), services (business logic), and models (data structure). Group related endpoints, use middleware for cross-cutting concerns, implement consistent error handling, and document your API thoroughly. The folder structure should reflect this organization, making it intuitive for developers to navigate and maintain.

Related Post

EMB Global
Privacy Overview

This website uses cookies so that we can provide you with the best user experience possible. Cookie information is stored in your browser and performs functions such as recognising you when you return to our website and helping our team to understand which sections of the website you find most interesting and useful.