Introduction

Building a REST API is a fundamental skill for backend developers. In this tutorial, we'll create a complete REST API using Node.js and Express, covering CRUD operations, middleware, and error handling.

Project Setup

First, initialize a new Node.js project:

mkdir my-api
cd my-api
npm init -y
npm install express
npm install --save-dev nodemon

Basic Server Setup

Create server.js:

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

// Middleware
app.use(express.json());

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

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

Creating Routes

Let's create a simple blog API with posts:

// In-memory database (for demo purposes)
let posts = [
  { id: 1, title: 'First Post', content: 'Hello World' },
  { id: 2, title: 'Second Post', content: 'Learning Node.js' }
];

// GET all posts
app.get('/api/posts', (req, res) => {
  res.json(posts);
});

// GET single post
app.get('/api/posts/:id', (req, res) => {
  const post = posts.find(p => p.id === parseInt(req.params.id));
  
  if (!post) {
    return res.status(404).json({ error: 'Post not found' });
  }
  
  res.json(post);
});

// CREATE post
app.post('/api/posts', (req, res) => {
  const { title, content } = req.body;
  
  if (!title || !content) {
    return res.status(400).json({ error: 'Title and content required' });
  }
  
  const newPost = {
    id: posts.length + 1,
    title,
    content
  };
  
  posts.push(newPost);
  res.status(201).json(newPost);
});

// UPDATE post
app.put('/api/posts/:id', (req, res) => {
  const post = posts.find(p => p.id === parseInt(req.params.id));
  
  if (!post) {
    return res.status(404).json({ error: 'Post not found' });
  }
  
  const { title, content } = req.body;
  
  if (title) post.title = title;
  if (content) post.content = content;
  
  res.json(post);
});

// DELETE post
app.delete('/api/posts/:id', (req, res) => {
  const index = posts.findIndex(p => p.id === parseInt(req.params.id));
  
  if (index === -1) {
    return res.status(404).json({ error: 'Post not found' });
  }
  
  posts.splice(index, 1);
  res.status(204).send();
});

Adding Middleware

Create custom middleware for logging and error handling:

// Logger middleware
const logger = (req, res, next) => {
  console.log(`${req.method} ${req.url} - ${new Date().toISOString()}`);
  next();
};

// Error handler middleware
const errorHandler = (err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ error: 'Something went wrong!' });
};

// Use middleware
app.use(logger);
app.use(errorHandler);

Input Validation

Add validation using express-validator:

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

app.post('/api/posts',
  [
    body('title')
      .trim()
      .isLength({ min: 3, max: 100 })
      .withMessage('Title must be 3-100 characters'),
    body('content')
      .trim()
      .isLength({ min: 10 })
      .withMessage('Content must be at least 10 characters')
  ],
  (req, res) => {
    const errors = validationResult(req);
    
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    
    // Create post...
  }
);

Organizing Routes

For larger applications, organize routes into separate files:

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

router.get('/', (req, res) => {
  // Get all posts
});

router.post('/', (req, res) => {
  // Create post
});

module.exports = router;

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

Database Integration

Connect to MongoDB using Mongoose:

const mongoose = require('mongoose');

mongoose.connect('mongodb://localhost/my-api', {
  useNewUrlParser: true,
  useUnifiedTopology: true
});

// Define schema
const postSchema = new mongoose.Schema({
  title: {
    type: String,
    required: true,
    minlength: 3,
    maxlength: 100
  },
  content: {
    type: String,
    required: true,
    minlength: 10
  },
  createdAt: {
    type: Date,
    default: Date.now
  }
});

const Post = mongoose.model('Post', postSchema);

// Use in routes
app.post('/api/posts', async (req, res) => {
  try {
    const post = new Post({
      title: req.body.title,
      content: req.body.content
    });
    
    await post.save();
    res.status(201).json(post);
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

Testing the API

Test your endpoints using curl:

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

# Create a post
curl -X POST http://localhost:3000/api/posts \
  -H "Content-Type: application/json" \
  -d '{"title":"New Post","content":"This is a new post"}'

# Update a post
curl -X PUT http://localhost:3000/api/posts/1 \
  -H "Content-Type: application/json" \
  -d '{"title":"Updated Title"}'

# Delete a post
curl -X DELETE http://localhost:3000/api/posts/1

Best Practices

  1. Use environment variables for configuration
  2. Implement proper error handling at all levels
  3. Validate input data before processing
  4. Use async/await for asynchronous operations
  5. Add rate limiting to prevent abuse
  6. Implement authentication for protected routes
  7. Use HTTPS in production
  8. Add request logging for debugging

Conclusion

You've now built a complete REST API with Express! This foundation can be extended with authentication, real databases, caching, and many other features.

Next Steps

  • Add JWT authentication
  • Implement pagination for list endpoints
  • Add search and filtering
  • Set up automated testing
  • Deploy to a cloud platform

Resources