Build REST API with Node.js & Express: Step-by-Step Guide

Introduction

Learn to build a professional REST API using Node.js and Express. This comprehensive guide covers everything from setup to deployment with real-world examples.

Written At

2025-06-17

Updated At

2025-06-17

Reading time

8 minutes

Step 1: Set Up Your Development Environment

Why it matters: A proper development environment ensures you have all the necessary tools and dependencies to build your API efficiently.

What to do:

  1. Install Node.js from nodejs.org (version 16 or higher recommended).
  2. Create a new project directory and initialize it:
    bash
    mkdir my-api-project
    cd my-api-project
    npm init -y
  3. Install essential dependencies:
    bash
    npm install express cors dotenv mongoose
    npm install --save-dev nodemon

Example:

Your package.json should include these scripts:

json
{
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js"
  }
}

Step 2: Create the Basic Server Structure

Why it matters: A well-structured server provides a solid foundation for your API and makes it easier to add features later.

What to do:

  1. Create the main server file:
    javascript
    // server.js
    const express = require('express');
    const cors = require('cors');
    const dotenv = require('dotenv');
    
    // Load environment variables
    dotenv.config();
    
    const app = express();
    const PORT = process.env.PORT || 3000;
    
    // Middleware
    app.use(cors());
    app.use(express.json());
    app.use(express.urlencoded({ extended: true }));
    
    // Basic route
    app.get('/', (req, res) => {
      res.json({ message: 'Welcome to my API!' });
    });
    
    // Start server
    app.listen(PORT, () => {
      console.log(`Server running on port ${PORT}`);
    });
  2. Create a .env file for environment variables:
    bash
    # .env
    PORT=3000
    MONGODB_URI=mongodb://localhost:27017/myapi
    JWT_SECRET=your-secret-key

Example:

Test your server by running npm run dev and visiting http://localhost:3000

Step 3: Set Up Database Connection with MongoDB

Why it matters: A database allows you to persist data and build dynamic applications that can handle real user data.

What to do:

  1. Create a database configuration file:
    javascript
    // config/database.js
    const mongoose = require('mongoose');
    
    const connectDB = async () => {
      try {
        const conn = await mongoose.connect(process.env.MONGODB_URI);
        console.log(`MongoDB Connected: ${conn.connection.host}`);
      } catch (error) {
        console.error(`Error: ${error.message}`);
        process.exit(1);
      }
    };
    
    module.exports = connectDB;
  2. Update your server.js to include database connection:
    javascript
    // Add this to server.js
    const connectDB = require('./config/database');
    
    // Connect to database
    connectDB();

Example:

Make sure MongoDB is running locally or use MongoDB Atlas for cloud hosting.

Step 4: Create Data Models and Schemas

Why it matters: Models define the structure of your data and provide methods for database operations.

What to do:

  1. Create a User model:
    javascript
    // models/User.js
    const mongoose = require('mongoose');
    
    const userSchema = new mongoose.Schema({
      name: {
        type: String,
        required: true,
        trim: true
      },
      email: {
        type: String,
        required: true,
        unique: true,
        lowercase: true
      },
      password: {
        type: String,
        required: true,
        minlength: 6
      }
    }, {
      timestamps: true
    });
    
    module.exports = mongoose.model('User', userSchema);
  2. Create a Post model:
    javascript
    // models/Post.js
    const mongoose = require('mongoose');
    
    const postSchema = new mongoose.Schema({
      title: {
        type: String,
        required: true,
        trim: true
      },
      content: {
        type: String,
        required: true
      },
      author: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User',
        required: true
      },
      tags: [String]
    }, {
      timestamps: true
    });
    
    module.exports = mongoose.model('Post', postSchema);

Example:

These models will automatically create collections in MongoDB with the proper structure and validation.

Step 5: Create API Routes and Controllers

Why it matters: Routes define the endpoints of your API, and controllers handle the business logic for each request.

What to do:

  1. Create a user controller:
    javascript
    // controllers/userController.js
    const User = require('../models/User');
    
    // Get all users
    const getUsers = async (req, res) => {
      try {
        const users = await User.find().select('-password');
        res.json(users);
      } catch (error) {
        res.status(500).json({ message: error.message });
      }
    };
    
    // Create new user
    const createUser = async (req, res) => {
      try {
        const user = new User(req.body);
        const savedUser = await user.save();
        res.status(201).json(savedUser);
      } catch (error) {
        res.status(400).json({ message: error.message });
      }
    };
    
    module.exports = {
      getUsers,
      createUser
    };
  2. Create user routes:
    javascript
    // routes/userRoutes.js
    const express = require('express');
    const router = express.Router();
    const { getUsers, createUser } = require('../controllers/userController');
    
    router.get('/', getUsers);
    router.post('/', createUser);
    
    module.exports = router;

Example:

Add the routes to your server.js:

javascript
// Add to server.js
const userRoutes = require('./routes/userRoutes');
app.use('/api/users', userRoutes);

Step 6: Add Authentication and Authorization

Why it matters: Authentication ensures only authorized users can access certain endpoints, protecting your API from unauthorized access.

What to do:

  1. Install JWT for token-based authentication:
    bash
    npm install jsonwebtoken bcryptjs
  2. Create authentication middleware:
    javascript
    // middleware/auth.js
    const jwt = require('jsonwebtoken');
    const User = require('../models/User');
    
    const protect = async (req, res, next) => {
      let token;
    
      if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
        try {
          // Get token from header
          token = req.headers.authorization.split(' ')[1];
    
          // Verify token
          const decoded = jwt.verify(token, process.env.JWT_SECRET);
    
          // Get user from token
          req.user = await User.findById(decoded.id).select('-password');
    
          next();
        } catch (error) {
          res.status(401).json({ message: 'Not authorized' });
        }
      }
    
      if (!token) {
        res.status(401).json({ message: 'Not authorized, no token' });
      }
    };
    
    module.exports = { protect };

Example:

Use the middleware to protect routes:

javascript
// In your routes
const { protect } = require('../middleware/auth');
router.get('/profile', protect, getProfile);

Step 7: Add Error Handling and Validation

Why it matters: Proper error handling improves user experience and makes debugging easier.

What to do:

  1. Create a custom error handler:
    javascript
    // middleware/errorHandler.js
    const errorHandler = (err, req, res, next) => {
      let error = { ...err };
      error.message = err.message;
    
      // Log error
      console.log(err);
    
      // Mongoose bad ObjectId
      if (err.name === 'CastError') {
        const message = 'Resource not found';
        error = new Error(message);
        error.statusCode = 404;
      }
    
      // Mongoose duplicate key
      if (err.code === 11000) {
        const message = 'Duplicate field value entered';
        error = new Error(message);
        error.statusCode = 400;
      }
    
      res.status(error.statusCode || 500).json({
        success: false,
        error: error.message || 'Server Error'
      });
    };
    
    module.exports = errorHandler;
  2. Add the error handler to your server:
    javascript
    // Add to server.js (after all routes)
    const errorHandler = require('./middleware/errorHandler');
    app.use(errorHandler);

Example:

This error handler will catch and format all errors consistently across your API.

Step 8: Test and Deploy Your API

Why it matters: Testing ensures your API works correctly, and deployment makes it accessible to users.

What to do:

  1. Test your API endpoints using tools like Postman or curl:
    bash
    # Test GET request
    curl http://localhost:3000/api/users
    
    # Test POST request
    curl -X POST http://localhost:3000/api/users \
      -H "Content-Type: application/json" \
      -d '{"name":"John Doe","email":"john@example.com","password":"123456"}'
  2. Deploy to a cloud platform like Heroku or Railway:
    json
    // package.json scripts
    {
      "scripts": {
        "start": "node server.js",
        "dev": "nodemon server.js",
        "build": "npm install"
      }
    }

Example:

Create a Procfile for Heroku deployment:

bash
# Procfile
web: npm start