Back to Blog
API testing
API design
API marketplace

Building RESTful APIs with Node.js and Express

6 min read
K
Kevin
API Security Specialist

Building RESTful APIs with Node.js and Express

Introduction to RESTful APIs with Node.js

RESTful APIs built with Node.js and Express remain a popular choice for backend development due to their simplicity, scalability, and performance. Express provides a minimal framework for building web applications while allowing developers to add middleware and routing as needed.

Key characteristics of RESTful APIs:

  • Stateless client-server communication
  • Standard HTTP methods (GET, POST, PUT, DELETE)
  • Resource-based URLs
  • JSON payloads (typically)

Setting Up the Project

Initialize a new Node.js project:

mkdir express-api
cd express-api
npm init -y
npm install express body-parser cors
npm install --save-dev nodemon

Basic Express server setup (server.js):

const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');

const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(cors());
app.use(bodyParser.json());

// Basic route
app.get('/', (req, res) => {
  res.json({ message: 'API is running' });
});

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

RESTful Routing and Controllers

Resource-Based Routing

Express allows clean routing structure for RESTful resources. For a products resource:

// routes/products.js
const express = require('express');
const router = express.Router();

// Controller would be imported here
const productsController = require('../controllers/products');

router.get('/', productsController.getAllProducts);
router.post('/', productsController.createProduct);
router.get('/:id', productsController.getProductById);
router.put('/:id', productsController.updateProduct);
router.delete('/:id', productsController.deleteProduct);

module.exports = router;

Controller Implementation

// controllers/products.js
let products = [
  { id: 1, name: 'Laptop', price: 999.99 },
  { id: 2, name: 'Phone', price: 699.99 }
];

exports.getAllProducts = (req, res) => {
  res.json(products);
};

exports.getProductById = (req, res) => {
  const product = products.find(p => p.id === parseInt(req.params.id));
  if (!product) return res.status(404).json({ message: 'Product not found' });
  res.json(product);
};

exports.createProduct = (req, res) => {
  const { name, price } = req.body;
  if (!name || !price) {
    return res.status(400).json({ message: 'Name and price are required' });
  }
  
  const newProduct = {
    id: products.length + 1,
    name,
    price: parseFloat(price)
  };
  
  products.push(newProduct);
  res.status(201).json(newProduct);
};

Middleware for API Development

Request Validation

Create validation middleware:

// middleware/validateProduct.js
exports.validateProduct = (req, res, next) => {
  const { name, price } = req.body;
  
  if (!name || typeof name !== 'string') {
    return res.status(400).json({ error: 'Invalid product name' });
  }
  
  if (!price || isNaN(parseFloat(price))) {
    return res.status(400).json({ error: 'Invalid price' });
  }
  
  next();
};

Error Handling

Centralized error handling middleware:

// middleware/errorHandler.js
exports.errorHandler = (err, req, res, next) => {
  console.error(err.stack);
  
  if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
    return res.status(400).json({ error: 'Invalid JSON payload' });
  }
  
  res.status(500).json({ error: 'Something went wrong' });
};

Register middleware in main app:

// server.js
const { errorHandler } = require('./middleware/errorHandler');
const { validateProduct } = require('./middleware/validateProduct');

// Apply to specific routes
app.post('/products', validateProduct, productsController.createProduct);

// Global error handler (must be last middleware)
app.use(errorHandler);

Database Integration

Using MongoDB with Mongoose

Install MongoDB dependencies:

npm install mongoose

Database connection setup:

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

const connectDB = async () => {
  try {
    await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/products_api', {
      useNewUrlParser: true,
      useUnifiedTopology: true
    });
    console.log('MongoDB connected');
  } catch (err) {
    console.error('Database connection error:', err);
    process.exit(1);
  }
};

module.exports = connectDB;

Product Model

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

const productSchema = new mongoose.Schema({
  name: {
    type: String,
    required: [true, 'Product name is required'],
    trim: true,
    maxlength: [100, 'Name cannot exceed 100 characters']
  },
  price: {
    type: Number,
    required: [true, 'Price is required'],
    min: [0, 'Price must be positive']
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

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

Updated Controller with Database

// controllers/products.js
const Product = require('../models/Product');

exports.getAllProducts = async (req, res, next) => {
  try {
    const products = await Product.find();
    res.json(products);
  } catch (err) {
    next(err);
  }
};

exports.createProduct = async (req, res, next) => {
  try {
    const product = new Product(req.body);
    await product.save();
    res.status(201).json(product);
  } catch (err) {
    next(err);
  }
};

API Documentation with OpenAPI/Swagger

Install Swagger dependencies:

npm install swagger-ui-express swagger-jsdoc

Swagger setup:

// config/swagger.js
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');

const options = {
  definition: {
    openapi: '3.0.0',
    info: {
      title: 'Products API',
      version: '1.0.0',
      description: 'A simple products API'
    },
    servers: [
      {
        url: 'http://localhost:3000/api/v1'
      }
    ]
  },
  apis: ['./routes/*.js']
};

const specs = swaggerJsdoc(options);

module.exports = (app) => {
  app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
};
/**
 * @swagger
 * /products:
 *   get:
 *     summary: Returns all products
 *     responses:
 *       200:
 *         description: A list of products
 *         content:
 *           application/json:
 *             schema:
 *               type: array
 *               items:
 *                 $ref: '#/components/schemas/Product'
 */
router.get('/', productsController.getAllProducts);

Authentication and Authorization

JWT Authentication

Install dependencies:

npm install jsonwebtoken bcryptjs

User model:

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

const userSchema = new mongoose.Schema({
  email: {
    type: String,
    required: true,
    unique: true,
    lowercase: true
  },
  password: {
    type: String,
    required: true,
    minlength: 6,
    select: false
  }
});

userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  this.password = await bcrypt.hash(this.password, 12);
  next();
});

module.exports = mongoose.model('User', userSchema);

Auth middleware:

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

exports.protect = async (req, res, next) => {
  let token;
  
  if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
    token = req.headers.authorization.split(' ')[1];
  }
  
  if (!token) {
    return res.status(401).json({ error: 'Not authorized' });
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = await User.findById(decoded.id);
    next();
  } catch (err) {
    res.status(401).json({ error: 'Invalid token' });
  }
};

API Versioning

Implement versioning in routes:

// server.js
const v1Router = require('./routes/v1');
app.use('/api/v1', v1Router);

Folder structure:

routes/
  v1/
    products.js
    users.js
  v2/
    products.js

Testing the API

Unit Testing with Jest

Install testing dependencies:

npm install --save-dev jest supertest mongodb-memory-server

Test setup:

// tests/products.test.js
const request = require('supertest');
const app = require('../server');
const Product = require('../models/Product');

describe('Products API', () => {
  beforeEach(async () => {
    await Product.deleteMany();
  });

  it('should create a new product', async () => {
    const res = await request(app)
      .post('/api/v1/products')
      .send({
        name: 'Test Product',
        price: 19.99
      });
    
    expect(res.statusCode).toEqual(201);
    expect(res.body).toHaveProperty('_id');
    expect(res.body.name).toBe('Test Product');
  });
});

Deployment Considerations

Environment Configuration

Use dotenv for environment variables:

npm install dotenv

.env file:

NODE_ENV=development
PORT=3000
MONGODB_URI=mongodb://localhost:27017/products_api
JWT_SECRET=your-secret-key

Production Best Practices

  1. Security:

    • Use HTTPS
    • Implement rate limiting
    • Sanitize user input
    • Use Helmet middleware
  2. Performance:

    • Implement caching
    • Use compression middleware
    • Enable gzip compression
  3. Monitoring:

    • Logging (Winston or Morgan)
    • Health checks
    • Error tracking (Sentry)

The flexibility of Express combined with Node.js's performance makes this stack ideal for building scalable APIs that can evolve with your application's requirements.

Back to Blog