Building REST APIs with Node.js

API Expert 18 min read Advanced 1,120 views
REST API Development

What is a REST API?

REST (Representational State Transfer) is an architectural style for designing networked applications. A REST API uses HTTP requests to GET, PUT, POST, and DELETE data, making it the standard for building web services.

REST Principles

  • Stateless: Each request contains all information needed to understand and process it
  • Client-Server: Separation of concerns between client and server
  • Cacheable: Responses should indicate if they can be cached
  • Uniform Interface: Standardized methods and conventions
  • Layered System: Architecture composed of hierarchical layers

Setting Up the Project

mkdir nodejs-rest-api
cd nodejs-rest-api
npm init -y
npm install express mongoose cors helmet morgan dotenv

API Design Best Practices

Use Proper HTTP Methods

  • GET: Retrieve resources
  • POST: Create new resources
  • PUT: Update entire resources
  • PATCH: Partial updates
  • DELETE: Remove resources

Use Nouns, Not Verbs

// Good
GET /users
POST /users
PUT /users/123
DELETE /users/123

// Bad
GET /getAllUsers
POST /createUser
PUT /updateUser/123

Use Plural Nouns for Collections

/users        // Collection of users
/users/123    // Specific user
/users/123/posts // Posts by specific user

Building the API

Basic Express Setup

const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');

const app = express();

// Middleware
app.use(helmet()); // Security headers
app.use(cors()); // Enable CORS
app.use(morgan('combined')); // Logging
app.use(express.json()); // Parse JSON bodies
app.use(express.urlencoded({ extended: true })); // Parse URL-encoded bodies

Database Model

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
    },
    age: Number,
    createdAt: {
        type: Date,
        default: Date.now
    }
});

const User = mongoose.model('User', userSchema);

Controller Functions

// Get all users
exports.getAllUsers = async (req, res) => {
    try {
        const users = await User.find();
        res.status(200).json({
            status: 'success',
            results: users.length,
            data: { users }
        });
    } catch (error) {
        res.status(500).json({
            status: 'error',
            message: error.message
        });
    }
};

// Get user by ID
exports.getUserById = async (req, res) => {
    try {
        const user = await User.findById(req.params.id);
        if (!user) {
            return res.status(404).json({
                status: 'fail',
                message: 'User not found'
            });
        }
        res.status(200).json({
            status: 'success',
            data: { user }
        });
    } catch (error) {
        res.status(500).json({
            status: 'error',
            message: error.message
        });
    }
};

// Create user
exports.createUser = async (req, res) => {
    try {
        const newUser = await User.create(req.body);
        res.status(201).json({
            status: 'success',
            data: { user: newUser }
        });
    } catch (error) {
        res.status(400).json({
            status: 'fail',
            message: error.message
        });
    }
};

// Update user
exports.updateUser = async (req, res) => {
    try {
        const user = await User.findByIdAndUpdate(
            req.params.id,
            req.body,
            { new: true, runValidators: true }
        );
        if (!user) {
            return res.status(404).json({
                status: 'fail',
                message: 'User not found'
            });
        }
        res.status(200).json({
            status: 'success',
            data: { user }
        });
    } catch (error) {
        res.status(400).json({
            status: 'fail',
            message: error.message
        });
    }
};

// Delete user
exports.deleteUser = async (req, res) => {
    try {
        const user = await User.findByIdAndDelete(req.params.id);
        if (!user) {
            return res.status(404).json({
                status: 'fail',
                message: 'User not found'
            });
        }
        res.status(204).json({
            status: 'success',
            data: null
        });
    } catch (error) {
        res.status(500).json({
            status: 'error',
            message: error.message
        });
    }
};

Routes

const express = require('express');
const userController = require('./userController');

const router = express.Router();

// GET /api/users
router.get('/', userController.getAllUsers);

// GET /api/users/:id
router.get('/:id', userController.getUserById);

// POST /api/users
router.post('/', userController.createUser);

// PUT /api/users/:id
router.put('/:id', userController.updateUser);

// DELETE /api/users/:id
router.delete('/:id', userController.deleteUser);

module.exports = router;

Main App File

const express = require('express');
const mongoose = require('mongoose');
const userRoutes = require('./routes/userRoutes');

const app = express();

// Connect to MongoDB
mongoose.connect(process.env.DATABASE_URL, {
    useNewUrlParser: true,
    useUnifiedTopology: true
});

// Routes
app.use('/api/users', userRoutes);

// Error handling middleware
app.use((err, req, res, next) => {
    err.statusCode = err.statusCode || 500;
    err.status = err.status || 'error';
    res.status(err.statusCode).json({
        status: err.status,
        message: err.message
    });
});

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

Advanced Features

Input Validation

const { body, validationResult } = require('express-validator');

const validateUser = [
    body('name').isLength({ min: 2 }).withMessage('Name must be at least 2 characters'),
    body('email').isEmail().withMessage('Please provide a valid email'),
    body('age').isInt({ min: 0, max: 120 }).withMessage('Age must be between 0 and 120')
];

// Use in route
router.post('/', validateUser, userController.createUser);

Response Format Standardization

// Success response format
{
    "status": "success",
    "data": {
        "user": { ... }
    }
}

// Error response format
{
    "status": "error",
    "message": "User not found"
}

Rate Limiting

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

const limiter = rateLimit({
    max: 100, // limit each IP to 100 requests per windowMs
    windowMs: 15 * 60 * 1000, // 15 minutes
    message: 'Too many requests from this IP, please try again later.'
});

app.use('/api/', limiter);

API Documentation

Consider using Swagger/OpenAPI for API documentation:

npm install swagger-ui-express swagger-jsdoc

const swaggerUi = require('swagger-ui-express');
const swaggerJsdoc = require('swagger-jsdoc');

const options = {
    definition: {
        openapi: '3.0.0',
        info: {
            title: 'Node.js REST API',
            version: '1.0.0',
        },
    },
    apis: ['./routes/*.js'],
};

const specs = swaggerJsdoc(options);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));

Testing the API

Using curl

# Get all users
curl http://localhost:3000/api/users

# Create a user
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"John Doe","email":"john@example.com","age":30}'

# Get user by ID
curl http://localhost:3000/api/users/1

# Update user
curl -X PUT http://localhost:3000/api/users/1 \
  -H "Content-Type: application/json" \
  -d '{"name":"Jane Doe"}'

# Delete user
curl -X DELETE http://localhost:3000/api/users/1

Using Postman

Import the following collection into Postman:

{
    "info": {
        "name": "Node.js REST API",
        "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
    },
    "item": [
        {
            "name": "Get All Users",
            "request": {
                "method": "GET",
                "url": "http://localhost:3000/api/users"
            }
        },
        {
            "name": "Create User",
            "request": {
                "method": "POST",
                "url": "http://localhost:3000/api/users",
                "body": {
                    "mode": "raw",
                    "raw": "{\"name\":\"John Doe\",\"email\":\"john@example.com\",\"age\":30}"
                }
            }
        }
    ]
}

Best Practices Summary

  • Use proper HTTP methods and status codes
  • Implement proper error handling
  • Validate input data
  • Use consistent response formats
  • Implement rate limiting
  • Add proper logging
  • Write comprehensive tests
  • Document your API

Next Steps

Conclusion

Building REST APIs with Node.js and Express provides a solid foundation for modern web applications. By following these best practices, you can create robust, scalable, and maintainable APIs that serve your applications effectively.