Building a RESTful API from Scratch with Node.js (Part 2)
APIs tutorialcreate an APIAPI development guideTutorial

5 min read

Adding MongoDB to Your Node.js API

This tutorial builds on our previous API project, showing how to replace the in-memory storage with MongoDB. We'll use Mongoose as our ODM (Object Data Modeling) library to interact with MongoDB.

Prerequisites

  • Completed Part 1 of the tutorial
  • MongoDB installed locally or a MongoDB Atlas account
  • Basic understanding of databases and async/await

Project Setup

First, install the required dependencies:

npm install mongoose dotenv

Updated Project Structure

Add these new files to your project:

books-api/
├── src/
│   ├── config/
│   │   └── database.js
│   ├── models/
│   │   └── Book.js
│   ├── routes/
│   │   └── books.js
│   ├── middleware/
│   │   └── errorHandler.js
│   └── server.js
├── package.json
└── .env

Environment Setup

Create a .env file in your root directory:

MONGODB_URI=mongodb://localhost:27017/books_api
NODE_ENV=development
PORT=3000

Database Configuration

Create src/config/database.js:

const mongoose = require('mongoose');

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

module.exports = connectDB;

Book Model

Create src/models/Book.js:

const mongoose = require('mongoose');

const bookSchema = new mongoose.Schema({
    title: {
        type: String,
        required: [true, 'Title is required'],
        trim: true
    },
    author: {
        type: String,
        required: [true, 'Author is required'],
        trim: true
    },
    publishedYear: {
        type: Number,
        validate: {
            validator: function(value) {
                return value >= 1000 && value <= new Date().getFullYear();
            },
            message: props => `${props.value} is not a valid year!`
        }
    },
    isbn: {
        type: String,
        unique: true,
        sparse: true,
        trim: true
    }
}, {
    timestamps: true
});

// Add index for better search performance
bookSchema.index({ title: 'text', author: 'text' });

module.exports = mongoose.model('Book', bookSchema);

Updated Server File

Update src/server.js:

require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const connectDB = require('./config/database');
const booksRouter = require('./routes/books');
const errorHandler = require('./middleware/errorHandler');

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

// Connect to MongoDB
connectDB();

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

// Routes
app.use('/api/books', booksRouter);

// Error handling middleware
app.use(errorHandler);

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

Updated Routes

Update src/routes/books.js:

const express = require('express');
const router = express.Router();
const Book = require('../models/Book');

// GET all books with pagination and filtering
router.get('/', async (req, res, next) => {
    try {
        const page = parseInt(req.query.page) || 1;
        const limit = parseInt(req.query.limit) || 10;
        const searchQuery = req.query.search;

        let query = {};
        
        // Add search functionality
        if (searchQuery) {
            query = { $text: { $search: searchQuery } };
        }

        const books = await Book.find(query)
            .skip((page - 1) * limit)
            .limit(limit)
            .sort({ createdAt: -1 });

        const total = await Book.countDocuments(query);

        res.json({
            books,
            currentPage: page,
            totalPages: Math.ceil(total / limit),
            totalBooks: total
        });
    } catch (error) {
        next(error);
    }
});

// GET book by ID
router.get('/:id', async (req, res, next) => {
    try {
        const book = await Book.findById(req.params.id);
        if (!book) {
            return res.status(404).json({ message: 'Book not found' });
        }
        res.json(book);
    } catch (error) {
        next(error);
    }
});

// POST new book
router.post('/', async (req, res, next) => {
    try {
        const book = new Book(req.body);
        const savedBook = await book.save();
        res.status(201).json(savedBook);
    } catch (error) {
        next(error);
    }
});

// PUT update book
router.put('/:id', async (req, res, next) => {
    try {
        const book = await Book.findByIdAndUpdate(
            req.params.id,
            req.body,
            { new: true, runValidators: true }
        );
        
        if (!book) {
            return res.status(404).json({ message: 'Book not found' });
        }
        
        res.json(book);
    } catch (error) {
        next(error);
    }
});

// DELETE book
router.delete('/:id', async (req, res, next) => {
    try {
        const book = await Book.findByIdAndDelete(req.params.id);
        
        if (!book) {
            return res.status(404).json({ message: 'Book not found' });
        }
        
        res.status(204).send();
    } catch (error) {
        next(error);
    }
});

module.exports = router;

Enhanced Error Handler

Update src/middleware/errorHandler.js:

function errorHandler(err, req, res, next) {
    console.error(err.stack);

    // Mongoose validation error
    if (err.name === 'ValidationError') {
        return res.status(400).json({
            message: 'Validation Error',
            errors: Object.values(err.errors).map(error => ({
                field: error.path,
                message: error.message
            }))
        });
    }

    // Mongoose duplicate key error
    if (err.code === 11000) {
        return res.status(400).json({
            message: 'Duplicate field value entered',
            field: Object.keys(err.keyPattern)[0]
        });
    }

    // Mongoose cast error (invalid ID)
    if (err.name === 'CastError') {
        return res.status(400).json({
            message: 'Invalid ID format'
        });
    }

    res.status(500).json({
        message: 'Something went wrong!',
        error: process.env.NODE_ENV === 'development' ? err.message : undefined
    });
}

module.exports = errorHandler;

Testing the MongoDB Integration

  1. Start MongoDB locally or ensure your Atlas connection is ready
  2. Start your server:
node src/server.js

Test the enhanced API endpoints:

# Get all books (with pagination)
curl "http://localhost:3000/api/books?page=1&limit=10"

# Search books
curl "http://localhost:3000/api/books?search=Gatsby"

# Create new book
curl -X POST -H "Content-Type: application/json" \
    -d '{
        "title": "The Great Gatsby",
        "author": "F. Scott Fitzgerald",
        "publishedYear": 1925,
        "isbn": "978-0743273565"
    }' \
    http://localhost:3000/api/books

# Update book
curl -X PUT -H "Content-Type: application/json" \
    -d '{
        "publishedYear": 1926
    }' \
    http://localhost:3000/api/books/[book-id]

# Delete book
curl -X DELETE http://localhost:3000/api/books/[book-id]

New Features Added

  1. MongoDB Integration

    • Proper database connection with error handling
    • Mongoose schema with validation
    • Timestamps for created and updated dates
  2. Enhanced Query Features

    • Pagination support
    • Text search functionality
    • Sorting by creation date
  3. Improved Error Handling

    • Mongoose validation errors
    • Duplicate key errors
    • Invalid ID format errors
  4. Data Validation

    • Required fields
    • Year validation
    • Unique ISBN

Best Practices Implemented

  1. Environment Variables

    • Database connection string
    • Node environment
    • Port configuration
  2. Error Handling

    • Centralized error handling middleware
    • Specific error types with appropriate status codes
    • Development vs production error messages
  3. Database Operations

    • Async/await for all database operations
    • Proper error catching
    • Validation before saving
  4. API Features

    • Pagination to handle large datasets
    • Search functionality
    • Proper HTTP status codes

Next Steps

To further enhance your API, consider:

  1. Implementing authentication and authorization
  2. Implementing rate limiting
  3. Adding API documentation using Swagger
  4. Setting up automated testing
  5. Adding pagination for GET requests
  6. Implementing proper logging

Conclusion

You now have a robust API with MongoDB integration! This implementation includes proper data persistence, validation, error handling, and several advanced features like pagination and search. Remember to:

  • Regularly backup your database
  • Monitor performance
  • Implement proper security measures
  • Keep your dependencies updated

The next tutorial will cover adding authentication and authorization to secure your API endpoints.

Related Posts

API Management Tools: A Comprehensive Overview
APIs tutorialcreate an APIAPI development guideTutorial

5 min read

APIs (Application Programming Interfaces) are the backbone of modern digital applications. They allow different software systems to communicate, exchange data, and collaborate seamlessly. As businesse...

API Security: Best Practices for Protecting Your Application
APIs tutorialcreate an APIAPI development guideTutorial

4 min read

In today’s interconnected digital world, APIs (Application Programming Interfaces) are the backbone of communication between different software applications. From mobile apps to cloud services, APIs e...

API Design Best Practices: Crafting Robust and Scalable APIs
APIs tutorialcreate an APIAPI development guideTutorial

5 min read

In the modern digital ecosystem, APIs (Application Programming Interfaces) serve as the backbone of connectivity. Whether you're building microservices, enabling integrations, or crafting data pipelin...