← Back to Tutorials
Node.js

Build a REST API with Authentication and JWT

Difficulty: Intermediate Est. Time: ~4 hours

Introduction

REST APIs are the backbone of modern web applications. In this tutorial, we'll build a production-ready REST API with complete authentication using JSON Web Tokens (JWT).

We'll implement user registration, login, protected routes, role-based access control, and best practices for API security.

What You'll Build
  • Complete REST API with Express
  • User registration and login
  • JWT-based authentication
  • Protected routes with middleware
  • Role-based access control (RBAC)
  • Input validation
What You'll Learn
  • REST API design principles
  • JWT token handling
  • Password hashing with bcrypt
  • Express middleware development
  • Input validation with Joi
  • API security best practices

Project Overview

Our REST API will manage resources with full CRUD operations and authentication.

API Endpoints

Method Endpoint Description Auth
POST /api/auth/register Register new user No
POST /api/auth/login Login user No
GET /api/auth/me Get current user Yes
GET /api/users List all users Admin
GET /api/products List products Yes
POST /api/products Create product Admin

Prerequisites

  • Node.js installed - Version 14+
  • MongoDB - Local or Atlas
  • Postman - For testing
  • Basic JavaScript knowledge

Project Setup

Bash
# Create project directory
mkdir rest-api-jwt
cd rest-api-jwt

# Initialize Node.js
npm init -y

# Install dependencies
npm install express mongoose bcryptjs jsonwebtoken cors dotenv joi helmet express-rate-limit

# Install dev dependency
npm install --save-dev nodemon

Project Structure

File Structure
rest-api-jwt/
├── config/
│   └── db.js
├── controllers/
│   ├── authController.js
│   ├── userController.js
│   └── productController.js
├── middleware/
│   ├── auth.js
│   ├── admin.js
│   └── validate.js
├── models/
│   ├── User.js
│   └── Product.js
├── routes/
│   ├── auth.js
│   ├── users.js
│   └── products.js
├── .env
├── server.js
└── package.json
Environment
# .env
PORT=5000
NODE_ENV=development
MONGO_URI=mongodb://localhost:27017/restapi
JWT_SECRET=your_super_secret_jwt_key_change_this
JWT_EXPIRE=30d
BCRYPT_ROUND=10

Database Setup

Let's set up the MongoDB connection.

JavaScript
// config/db.js
const mongoose = require('mongoose');

const connectDB = async () => {
    try {
        const conn = await mongoose.connect(process.env.MONGO_URI);
        console.log(`MongoDB Connected: ${conn.connection.host}`);
    } catch (error) {
        console.error(`Error: ${error.message}`);
        process.exit(1);
    }
};

module.exports = connectDB;

Data Models

Let's create the Mongoose models for users and products.

JavaScript
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');

const userSchema = mongoose.Schema({
    name: {
        type: String,
        required: [true, 'Please provide a name'],
        maxlength: [50, 'Name cannot be more than 50 characters']
    },
    email: {
        type: String,
        required: [true, 'Please provide an email'],
        unique: true,
        match: [
            /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/,
            'Please provide a valid email'
        ]
    },
    password: {
        type: String,
        required: [true, 'Please add a password'],
        minlength: [6, 'Password must be at least 6 characters'],
        select: false
    },
    role: {
        type: String,
        enum: ['user', 'admin'],
        default: 'user'
    },
    avatar: {
        type: String,
        default: ''
    },
    resetPasswordToken: String,
    resetPasswordExpire: Date
}, {
    timestamps: true
});

// Encrypt password using bcrypt
userSchema.pre('save', async function(next) {
    if (!this.isModified('password')) {
        next();
    }
    const salt = await bcrypt.genSalt(10);
    this.password = await bcrypt.hash(this.password, salt);
});

// Sign JWT and return
userSchema.methods.getSignedJwtToken = function() {
    return jwt.sign({ id: this._id }, process.env.JWT_SECRET, {
        expiresIn: process.env.JWT_EXPIRE
    });
};

// Match password
userSchema.methods.matchPassword = async function(enteredPassword) {
    return await bcrypt.compare(enteredPassword, this.password);
};

// Generate reset password token
userSchema.methods.getResetPasswordToken = function() {
    const crypto = require('crypto');
    
    // Generate token
    const resetToken = crypto.randomBytes(20).toString('hex');
    
    // Hash token and set to resetPasswordToken field
    this.resetPasswordToken = crypto
        .createHash('sha256')
        .update(resetToken)
        .digest('hex');
    
    // Set expire
    this.resetPasswordExpire = Date.now() + 10 * 60 * 1000;
    
    return resetToken;
};

module.exports = mongoose.model('User', userSchema);
JavaScript
// models/Product.js
const mongoose = require('mongoose');

const productSchema = mongoose.Schema({
    user: {
        type: mongoose.Schema.ObjectId,
        required: true,
        ref: 'User'
    },
    name: {
        type: String,
        required: [true, 'Please add a product name'],
        trim: true
    },
    description: {
        type: String,
        required: [true, 'Please add a description']
    },
    price: {
        type: Number,
        required: [true, 'Please add a price'],
        min: 0
    },
    category: {
        type: String,
        required: [true, 'Please add a category'],
        enum: ['electronics', 'clothing', 'books', 'other']
    },
    stock: {
        type: Number,
        required: true,
        min: 0,
        default: 0
    },
    image: {
        type: String,
        default: ''
    }
}, {
    timestamps: true
});

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

Authentication Middleware

Let's create the middleware to protect routes and check roles.

JavaScript
// middleware/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User');

const protect = async (req, res, next) => {
    let token;

    // Check for bearer token in header
    if (req.headers.authorization && 
        req.headers.authorization.startsWith('Bearer')) {
        token = req.headers.authorization.split(' ')[1];
    }

    // Make sure token exists
    if (!token) {
        return res.status(401).json({
            success: false,
            error: 'Not authorized to access this route'
        });
    }

    try {
        // Verify token
        const decoded = jwt.verify(token, process.env.JWT_SECRET);

        // Get user from token
        req.user = await User.findById(decoded.id);
        
        if (!req.user) {
            return res.status(404).json({
                success: false,
                error: 'No user found with this id'
            });
        }

        next();
    } catch (err) {
        return res.status(401).json({
            success: false,
            error: 'Not authorized to access this route'
        });
    }
};

module.exports = { protect };
JavaScript
// middleware/admin.js
const protect = require('./auth');

const admin = (req, res, next) => {
    if (req.user && req.user.role === 'admin') {
        next();
    } else {
        return res.status(403).json({
            success: false,
            error: 'Not authorized as an admin'
        });
    }
};

module.exports = { protect, admin };
JavaScript
// middleware/validate.js
const Joi = require('joi');

const validate = (schema) => {
    return (req, res, next) => {
        const { error } = schema.validate(req.body);
        
        if (error) {
            const firstError = error.details.map(detail => detail.message).join(', ');
            return res.status(400).json({
                success: false,
                error: firstError
            });
        }
        
        next();
    };
};

// Validation schemas
const schemas = {
    register: Joi.object({
        name: Joi.string().max(50).required(),
        email: Joi.string().email().required(),
        password: Joi.string().min(6).required()
    }),
    
    login: Joi.object({
        email: Joi.string().email().required(),
        password: Joi.string().required()
    }),
    
    product: Joi.object({
        name: Joi.string().required(),
        description: Joi.string().required(),
        price: Joi.number().min(0).required(),
        category: Joi.string().valid('electronics', 'clothing', 'books', 'other').required(),
        stock: Joi.number().min(0).default(0)
    })
};

module.exports = { validate, schemas };
Security Best Practices
  • Always use HTTPS in production
  • Store JWT secret in environment variables
  • Set appropriate token expiration
  • Use HTTP-only cookies for sensitive apps

API Routes

Let's create the route handlers for authentication and resources.

JavaScript
// routes/auth.js
const express = require('express');
const router = express.Router();
const { protect } = require('../middleware/auth');
const { validate, schemas } = require('../middleware/validate');
const User = require('../models/User';

// @route   POST /api/auth/register
// @desc    Register user
// @access  Public
router.post('/register', validate(schemas.register), async (req, res) => {
    try {
        const { name, email, password } = req.body;

        // Check if user exists
        const existingUser = await User.findOne({ email });
        if (existingUser) {
            return res.status(400).json({
                success: false,
                error: 'Email already exists'
            });
        }

        // Create user
        const user = await User.create({ name, email, password });

        sendTokenResponse(user, 201, res);
    } catch (error) {
        res.status(500).json({
            success: false,
            error: error.message
        });
    }
});

// @route   POST /api/auth/login
// @desc    Login user
// @access  Public
router.post('/login', validate(schemas.login), async (req, res) => {
    try {
        const { email, password } = req.body;

        // Check for user
        const user = await User.findOne({ email }).select('+password');
        
        if (!user) {
            return res.status(401).json({
                success: false,
                error: 'Invalid credentials'
            });
        }

        // Check if password matches
        const isMatch = await user.matchPassword(password);
        
        if (!isMatch) {
            return res.status(401).json({
                success: false,
                error: 'Invalid credentials'
            });
        }

        sendTokenResponse(user, 200, res);
    } catch (error) {
        res.status(500).json({
            success: false,
            error: error.message
        });
    }
});

// @route   GET /api/auth/me
// @desc    Get current logged in user
// @access  Private
router.get('/me', protect, async (req, res) => {
    const user = await User.findById(req.user.id);
    
    res.status(200).json({
        success: true,
        data: user
    });
});

// Helper function to get token and send response
const sendTokenResponse = (user, statusCode, res) => {
    const token = user.getSignedJwtToken();
    
    const options = {
        expires: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
        httpOnly: true
    };
    
    if (process.env.NODE_ENV === 'production') {
        options.secure = true;
    }
    
    res
        .status(statusCode)
        .cookie('token', token, options)
        .json({
            success: true,
            token,
            data: {
                id: user._id,
                name: user.name,
                email: user.email,
                role: user.role
            }
        });
};

module.exports = router;
JavaScript
// routes/products.js
const express = require('express');
const router = express.Router();
const { protect, admin } = require('../middleware/admin');
const { validate, schemas } = require('../middleware/validate');
const Product = require('../models/Product';

// @route   GET /api/products
// @desc    Get all products
// @access  Private
router.get('/', protect, async (req, res) => {
    try {
        const products = await Product.find();
        
        res.status(200).json({
            success: true,
            count: products.length,
            data: products
        });
    } catch (error) {
        res.status(500).json({
            success: false,
            error: error.message
        });
    }
});

// @route   GET /api/products/:id
// @desc    Get single product
// @access  Private
router.get('/:id', protect, 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(500).json({
            success: false,
            error: error.message
        });
    }
});

// @route   POST /api/products
// @desc    Create product
// @access  Private (Admin only)
router.post('/', [protect, admin, validate(schemas.product)], async (req, res) => {
    try {
        const product = await Product.create({
            ...req.body,
            user: req.user.id
        });
        
        res.status(201).json({
            success: true,
            data: product
        });
    } catch (error) {
        res.status(500).json({
            success: false,
            error: error.message
        });
    }
});

// @route   PUT /api/products/:id
// @desc    Update product
// @access  Private (Admin only)
router.put('/:id', [protect, admin], async (req, res) => {
    try {
        let product = await Product.findById(req.params.id);
        
        if (!product) {
            return res.status(404).json({
                success: false,
                error: 'Product not found'
            });
        }
        
        product = await Product.findByIdAndUpdate(req.params.id, req.body, {
            new: true,
            runValidators: true
        });
        
        res.status(200).json({
            success: true,
            data: product
        });
    } catch (error) {
        res.status(500).json({
            success: false,
            error: error.message
        });
    }
});

// @route   DELETE /api/products/:id
// @desc    Delete product
// @access  Private (Admin only)
router.delete('/:id', [protect, admin], 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'
            });
        }
        
        await product.deleteOne();
        
        res.status(200).json({
            success: true,
            data: {}
        });
    } catch (error) {
        res.status(500).json({
            success: false,
            error: error.message
        });
    }
});

module.exports = router;

Main Server File

Let's put it all together in the main server file.

JavaScript
// server.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const connectDB = require('./config/db');
require('dotenv').config();

// Connect to database
connectDB();

const app = express();

// Body parser
app.use(express.json());

// Security middleware
app.use(helmet());
app.use(cors());

// Rate limiting
const limiter = rateLimit({
    windowMs: 10 * 60 * 1000,
    max: 100
});
app.use(limiter);

// Mount routes
app.use('/api/auth', require('./routes/auth'));
app.use('/api/users', require('./routes/users'));
app.use('/api/products', require('./routes/products'));

// Root route
app.get('/', (req, res) => {
    res.json({ message: 'Welcome to the API' });
});

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

const server = app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});

// Handle unhandled promise rejections
process.on('unhandledRejection', (err, promise) => {
    console.log(`Error: ${err.message}`);
    server.close(() => process.exit(1));
});

Testing the API

Bash
# Start the server
npm run dev

# Test endpoints with curl

# 1. Register a user
curl -X POST http://localhost:5000/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "name": "John Doe",
    "email": "john@example.com",
    "password": "password123"
  }'

# 2. Login
curl -X POST http://localhost:5000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "john@example.com",
    "password": "password123"
  }'

# 3. Get current user (with token)
curl http://localhost:5000/api/auth/me \
  -H "Authorization: Bearer YOUR_TOKEN"

# 4. Create a product (as admin)
curl -X POST http://localhost:5000/api/products \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
  -d '{
    "name": "Laptop",
    "description": "A powerful laptop",
    "price": 999,
    "category": "electronics",
    "stock": 10
  }'
Testing Checklist
  • ✓ User can register
  • ✓ User can login and receive JWT
  • ✓ Protected routes require token
  • ✓ Admin routes require admin role
  • ✓ Input validation works
  • ✓ CRUD operations work

Summary

Congratulations! You've built a complete REST API with authentication.

What You Built

  • REST API - Full CRUD operations
  • JWT Authentication - Secure login and registration
  • Protected Routes - Middleware for authorization
  • Role-Based Access - Admin and user roles
  • Input Validation - Joi validation
  • Security - Helmet, rate limiting

Next Steps

  • Add refresh tokens
  • Implement password reset
  • Add email verification
  • Add pagination and filtering

Continue Learning

Try these tutorials:

  • Build a Rate Limiter
  • Build a Real-Time Notification System
  • Build a File Upload Server