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

14 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

Conclusion:

Congratulations! You've successfully built a complete REST API with Node.js and Express. You now have a solid foundation that includes database integration, authentication, error handling, and proper project structure. This API can serve as a starting point for building more complex applications. Remember to add more features like file uploads, pagination, and advanced querying as your project grows. Keep learning and experimenting with new technologies to enhance your API further.

Related Blogs