DEV Community

William Onyejiaka
William Onyejiaka

Posted on

A Beginner’s Guide to Building a CRUD API with Express, TypeScript, and Mongoose

When building APIs with Node.js and TypeScript, Express is often the framework of choice for handling routes and middleware. To interact with MongoDB, we use Mongoose, a popular ODM (Object Data Modeling) library that provides a schema-based solution to model our data.

In this post, you’ll learn how to set up a simple Express + TypeScript project using Mongoose to perform CRUD operations — Create, Read, Update, and Delete — on a single resource.

We’ll build a simple API to manage a collection of Books, where each book has:

  • title

  • author

  • year

  • isPublished


What is Mongoose?

Mongoose simplifies working with MongoDB by providing:

  • Schema validation

  • Query helpers and middleware

  • Model methods

  • TypeScript type support

Essentially, it helps you structure your MongoDB collections more predictably and safely.


Prerequisites

  • Node.js (v16 or higher)

  • Basic knowledge of Express and TypeScript

  • MongoDB running locally or a MongoDB Atlas account

  • Code editor (e.g., VS Code)


Step 1: Set Up the Project

  1. Initialize the project:

Create a new directory and set up a Node.js project:

mkdir express-mongoose-ts
cd express-mongoose-ts
npm init -y
Enter fullscreen mode Exit fullscreen mode
  1. Install dependencies:

Install Express, TypeScript, and necessary type definitions:

npm install express mongoose
npm install -D typescript @types/node @types/express ts-node nodemon
Enter fullscreen mode Exit fullscreen mode
  1. Set up TypeScript:

Initialize TypeScript configuration:

npx tsc --init
Enter fullscreen mode Exit fullscreen mode

Update tsconfig.json to include:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "rootDir": "./src",
    "outDir": "./dist",
    "esModuleInterop": true,
    "strict": true
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Configure scripts:

Update package.json with:

"scripts": {
  "start": "node dist/server.js",
  "build": "tsc p .",
  "dev": "nodemon src/server.ts"
}
Enter fullscreen mode Exit fullscreen mode

Project Structure

Here’s how your folder should look:

express-mongoose-ts/
├── src/
│   ├── config/
│   │   └── db.ts
│   ├── models/
│   │   └── book.model.ts
│   ├── routes/
│   │   └── book.route.ts
│   ├── controllers/
│   │   └── book.controller.ts
│   └── server.ts
├── package.json
├── tsconfig.json
Enter fullscreen mode Exit fullscreen mode

Connecting to MongoDB

Insidesrc/config/db.ts:

import mongoose from "mongoose";

export async function connectDB(): Promise<void> {
  try {
    const conn = await mongoose.connect("mongodb://localhost:27017/express_mongoose_ts");
    console.log(`MongoDB connected: ${conn.connection.host}`);
  } catch (err) {
    console.error("Database connection failed:", err);
    process.exit(1);
  }
}

Enter fullscreen mode Exit fullscreen mode

Defining the Mongoose Model

Create src/models/book.model.ts:

import { Schema, model } from "mongoose";

const bookSchema = new Schema(
  {
    title: { type: String, required: true },
    author: { type: String, required: true },
    year: { type: Number, required: true },
    isPublished: { type: Boolean, default: false },
  },
  { timestamps: true }
);

export const Book = model("Book", bookSchema);
Enter fullscreen mode Exit fullscreen mode

Here, we:

  • Create a Mongoose schema

  • Export a model called Book


Writing the Controller Functions

Create src/controllers/book.controller.ts:

import { Request, Response } from "express";
import { Book } from "../models/book.model";

// Create a new book
export async function createBook(req: Request, res: Response) {
  try {
    const book = await Book.create(req.body);
    res.status(201).json(book);
  } catch (error) {
    res.status(400).json({ message: "Error creating book", error });
  }
}

// Get all books
export async function getBooks(req: Request, res: Response) {
  try {
    const books = await Book.find();
    res.status(200).json(books);
  } catch (error) {
    res.status(500).json({ message: "Error fetching books", error });
  }
}

// Get a single book by ID
export async function getBookById(req: Request, res: Response) {
  try {
    const book = await Book.findById(req.params.id);
    if (!book) return res.status(404).json({ message: "Book not found" });
    res.status(200).json(book);
  } catch (error) {
    res.status(500).json({ message: "Error fetching book", error });
  }
}

// Update a book
export async function updateBook(req: Request, res: Response) {
  try {
    const book = await Book.findByIdAndUpdate(req.params.id, req.body, {
      new: true,
    });
    if (!book) return res.status(404).json({ message: "Book not found" });
    res.status(200).json(book);
  } catch (error) {
    res.status(500).json({ message: "Error updating book", error });
  }
}

// Delete a book
export async function deleteBook(req: Request, res: Response) {
  try {
    const book = await Book.findByIdAndDelete(req.params.id);
    if (!book) return res.status(404).json({ message: "Book not found" });
    res.status(200).json({ message: "Book deleted successfully" });
  } catch (error) {
    res.status(500).json({ message: "Error deleting book", error });
  }
}

Enter fullscreen mode Exit fullscreen mode

Creating the Express Routes

Create src/routes/book.route.ts:

import { Router } from "express";
import {
  createBook,
  getBooks,
  getBookById,
  updateBook,
  deleteBook,
} from "../controllers/book.controller";

const router = Router();

router.post("/", createBook);
router.get("/", getBooks);
router.get("/:id", getBookById);
router.put("/:id", updateBook);
router.delete("/:id", deleteBook);

export default router;
Enter fullscreen mode Exit fullscreen mode

Setting Up the App Entry Point

Create src/server.ts:

import express, { Application } from "express";
import { connectDB } from "./config/db";
import bookRouter from "./routes/book.route";

const app: Application = express();

// Enable URL-encoded form data parsing
app.use(express.urlencoded({ extended: true }));

// Middleware to parse JSON
app.use(express.json());

// Routes
app.use("/api/books", bookRouter);

// Start Server
const PORT = process.env.PORT || 4000;

app.listen(PORT, async () => {
  await connectDB();
  console.log(`Server running on http://localhost:${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

Testing the API

Start the server:

npm run dev
Enter fullscreen mode Exit fullscreen mode

If you see:

MongoDB connected successfully
Server running on http://localhost:4000
Enter fullscreen mode Exit fullscreen mode

You’re ready to test using Postman or cURL.

Examples:

  • POST /api/books
{
  "title": "The Alchemist",
  "author": "Paulo Coelho",
  "year": 1988,
  "isPublished": true
}

Enter fullscreen mode Exit fullscreen mode
  • GET /api/books
    → Returns all books

  • GET /api/books/:id
    → Returns one book

  • PUT /api/books/:id
    → Updates a book

  • DELETE /api/books/:id
    → Deletes a book


Conclusion

You’ve just built a simple CRUD API using Express + Mongoose + TypeScript.
In a real-world project, you’d add features like:

  • Input validation with express-validator or zod

  • Environment variables using dotenv

  • Better error handling and logging

But this setup is a strong foundation for learning backend development with TypeScript and MongoDB.

The code for this blog is here.

Top comments (0)