How to Easily Build a Web API with Express and MongoDB [TypeScript Compatible]
Introduction
In recent years, web application development that leverages cloud databases and type-safe programming languages has been attracting attention. In particular, MongoDB Atlas makes it easy to manage databases in the cloud and is well-suited for building scalable applications. TypeScript is also widely adopted by developers because it improves code readability and maintainability.
In this article, we will explain concretely how to build a REST API by combining these technologies. We will proceed in a beginner-friendly, step-by-step format, from setting up MongoDB Atlas and routing with Express, to implementing type-safe logic using TypeScript. By reading this article, you will gain a basic understanding of API development using MongoDB Atlas and experience the benefits of type-safe development practices.
For those who are not yet very familiar with how Express works, we have prepared an article that explains the basic usage of Express.
Goal for This Tutorial
We will use MondbDB as the database, access it from Express, and store the results in the database.
In the end, we will access the Express server using the four methods GET, POST, PATCH, and DELETE to retrieve, create, update, and delete data.
Requests to the server will be made using the curl command.
Image of retrieving data with the GET method:
You can also check whether the data has been registered on MongoDB Atlas.
What is MongoDB?
MongoDB is a document-oriented NoSQL database and a database management system that excels in flexible data models and scalability. It has the following characteristics:
Main Features
-
Document-oriented
Data is stored as “BSON (Binary JSON)”, which is similar to JSON format. Instead of tables and rows (RDBMS), it uses the concepts of collections and documents. -
Schemaless
No explicit schema definition is required, and the data structure can be changed flexibly. It is robust against changing project requirements and scaling up. -
High scalability
Horizontal scaling is easy through sharding (splitting data). It can efficiently handle large volumes of data. -
High performance
High-speed read/write performance. Especially suitable for real-time data analysis and applications where the data structure changes frequently. -
Rich ecosystem
Provides drivers for major programming languages such as Node.js, Python, and Java. By using MongoDB Atlas (cloud service), you can use a database without building infrastructure.
What is MongoDB Atlas?
MongoDB Atlas is a cloud-based database service provided by MongoDB Inc. It is a fully managed database platform that takes care of time-consuming server setup and operations, and can be used on the following cloud providers:
- AWS (Amazon Web Services)
- GCP (Google Cloud Platform)
- Microsoft Azure
You could also choose to set up MongoDB by running Docker in your local environment, but since MongoDB Atlas is easier to get started with, we will use MongoDB Atlas this time.
MongoDB Atlas is free to use as long as storage is within 512MB.
Setting up mongoDB Atlas
Access the following and register an account.
Click New Project to go to the new project creation screen.
Set the project name to express-tutorial.
Click Create Project to create the project.
Once the project has been created, the next step is to create a cluster.
You can select the cluster specs.
This time, select M0, which is marked as Free, and create the cluster.
Configure the IP address and user that will connect to the cluster.
For the IP address, current IP Address should be fine for now. You will need to change this setting when you deploy Express somewhere.
Then set the username and password.
Once the configuration is complete, you will see Choose a connection method.
Select Drivers.
Select Mongoose.
You will need to set this in an environment variable, so make a note of it.
mongodb+srv://dbUser:<db_password>@cluster0.elo46.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0
Project Setup
Create a new project for Express and install the required packages.
mkdir express-mongodb-rest-api-development-with-typescript
cd express-mongodb-rest-api-development-with-typescript
npm init -y
Installing packages
npm i express cors dotenv express-async-handler mongoose
npm i -D @types/node @types/express @types/mongoose @types/cors rimraf @types/rimraf ts-node-dev typescript
express
- Purpose: Web framework for Node.js.
- Role:
- Routing (branching the handling of HTTP requests).
- Handling requests/responses using middleware.
- Useful for building REST APIs and web applications.
@types/express
- Purpose: Type definition file for Express.
- Role:
- Enables type-safe use of Express in TypeScript.
- Clarifies the types of arguments and return values for routes and middleware.
cors
- Purpose: Middleware for configuring Cross-Origin Resource Sharing (CORS).
- Role:
- Allows requests from other origins (domains).
- Enables secure integration between frontend and backend.
@types/cors
- Purpose: Type definition file for CORS.
- Role:
- Enables type-safe use of the CORS middleware in TypeScript.
- Makes configuration option types explicit.
dotenv
- Purpose: Package that loads environment variables from a
.envfile. - Role:
- Safely manages environment variables (API keys, database URLs, etc.).
- Simplifies switching settings between environments.
express-async-handler
- Purpose: Utility for easily handling asynchronous functions in Express.
- Role:
- Simplifies error handling in asynchronous functions.
- Passes errors to the error handler without writing
try-catch.
mongoose
- Purpose: Object Data Modeling (ODM) library for MongoDB.
- Role:
- Defines schemas and models.
- Simplifies queries to MongoDB.
- Provides mapping between documents and JavaScript objects.
@types/mongoose
- Purpose: Type definition file for Mongoose.
- Role:
- Enables type-safe use of Mongoose in TypeScript.
- Strengthens type definitions for models and schemas.
@types/node
- Purpose: Type definition file for Node.js.
- Role:
- Enables type-safe use of Node.js built-in modules (fs, http, path, etc.) in TypeScript.
rimraf
- Purpose: Utility for deleting folders and files.
- Role:
- Provides
rm -rf-like operations in a cross-platform way.
- Provides
@types/rimraf
- Purpose: Type definition file for rimraf.
- Role:
- Enables type-safe use of rimraf in TypeScript.
ts-node
- Purpose: Tool for running TypeScript code directly.
- Role:
- Executes TypeScript scripts without transpiling.
- Convenient during development.
ts-node-dev
- Purpose: Tool for hot reloading TypeScript code.
- Role:
- Automatically restarts the server when code changes.
- Improves development efficiency.
Creating tsconfig
Create a tsconfig.json file to manage the compiler settings for the TypeScript project.
npx tsc --init
Creating .env
Manage the MongoDB connection information obtained earlier in .env.
MONGO_URI=mongodb+srv://dbUser:dbUserPassword@cluster0.elo46.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0
Connecting to MongoDB from Express
MongoDB connection logic
Create the src/lib/db.ts file and write the code.
import mongoose from 'mongoose';
import dotenv from 'dotenv';
dotenv.config();
const DATABASE_URL = process.env.MONGO_URI!
const connectDB = async () => {
try {
const connection = await mongoose.connect(DATABASE_URL);
console.log(`MongoDB Connected ${connection.connection.host}`);
} catch (error) {
if (error instanceof Error) {
console.error(`Error: ${error.message}`);
} else {
console.error(`Unexpected Error: ${error}`);
}
process.exit(1);
}
};
export default connectDB;
Creating the Express entry point
import express from 'express';
import cors from 'cors';
import connectDB from './lib/db';
connectDB();
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.listen(3001);
console.log('Express WebAPI listening on port ' + port);
Configuring package.json
Configure the command to start the Express server in package.json.
"scripts": {
+ "dev": "ts-node-dev --respawn src/index.ts",
"test": "echo \"Error: no test specified\" && exit 1"
},
Operation check
Run the following command to start the Express server and connect to MongoDB.
npm run dev
If you see a message like the one below, both have succeeded.
Creating the Model
We will define the data to be stored in MongoDB.
This time, assuming we are creating a TODO app, we will prepare the following fields:
- Title: String
- Completion flag: Boolean (default: false)
- Created date: Date (default: registration time)
- Updated date: Date
import mongoose from 'mongoose';
const PostSchema = new mongoose.Schema(
{
title: {
type: String,
required: true,
},
isCompleted: {
type: Boolean,
required: true,
default: false,
},
createdAt: {
type: Date,
required: true,
default: Date.now,
},
updatedAt: {
type: Date,
},
},
);
const Post = mongoose.model('Post', PostSchema);
export default Post;
Registering Data
Using the model we just created, we will register initial data from the local environment to MongoDB.
Creating data for registration
Create two todos.
We omit isCompleted and createdAt here because default values are set for them.
export const todos = [
{
title: 'Reply to inquiries',
},
{
title: 'Write a blog post',
},
]
Creating the registration script
To make the registration process repeatable, we first delete all records and then insert them.
We also prepare a process for deleting all records in advance.
import dotenv from 'dotenv';
import { todos } from './todo';
import Todo from '../models/todo';
import connectDB from '../lib/db';
dotenv.config();
connectDB();
const importData = async () => {
try {
// First delete todos, then insert all
await Todo.deleteMany();
const createPosts = await Todo.insertMany(todos);
console.log(`Data Imported!`);
process.exit();
} catch (error) {
console.error(`Error: ${error}`);
process.exit(1);
}
};
const destroyData = async () => {
try {
await Todo.deleteMany();
console.log(`Data Destroyed!`);
process.exit();
} catch (error) {
console.error(`Error: ${error}`);
process.exit(1);
}
};
// If the -d parameter is passed on the command line, run in delete mode
if (process.argv[2] === '-d') {
destroyData();
} else {
importData();
}
Registering the data
Run the following command to register the data.
npx ts-node src/seeds/seed.ts
If Data Imported! is displayed, registration is complete.
You can also confirm that the data has been registered on MongoDB Atlas.
Now that the preparations are complete, we will move on to the implementation.
Retrieving Data (GET method)
First, we will implement a GET method to retrieve the registered data.
Creating the controller
Create the DB access logic in the controller.
import {Request, Response} from 'express';
import asyncHandler from 'express-async-handler';
import Todo from '../models/todo';
export const getPosts = asyncHandler(async (req: Request, res: Response) => {
const todos = await Todo.find();
res.json(todos);
});
Creating the routing
Define that when a GET request is received, the getTodos we just created will be called.
We will also group routes because we will create other routes such as POST methods later.
import express from 'express';
import { getTodos } from '../controllers/todo';
const router = express.Router();
router.route('/').get(getTodos)
export default router
Applying the routing to the entry point
Reflect the above in the entry point.
In this TODO app, all URLs will be set under /todos.
import express from 'express';
import cors from 'cors';
import connectDB from './lib/db';
+ import todoRoutes from './routes/todo'
connectDB();
const app = express();
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
+ app.use('/todos', todoRoutes)
app.listen(3001);
console.log('Express WebAPI listening on port 3001');
Express routing is explained in detail in the following article.
Operation check
Actually access the API and check whether the data can be retrieved.
We use jq to make the output easier to read.
curl -s http://localhost:3001/todos | jq
If the two todos registered in advance are output, it is OK.
Creating New Data (POST method)
The implementation is almost the same as the GET method.
export const addTodo = asyncHandler(async (req: express.Request, res: express.Response) => {
const newPost = new Post(req.body as PostType);
await newPost.save((error, post) => {
if (error) res.send(error);
res.json(post);
});
});
The access URL for the POST method will be the same as for the GET method.
- router.route('/').get(getTodos)
+ router.route('/').get(getTodos).post(addTodo)
Operation check
Send a request with the POST method.
curl -X POST -H "Content-Type: application/json" -d '{"title": "Post test"}' http://localhost:3001/todos
If registration is successful, the registered content will be returned.
Updating Data (PATCH method)
We search for the target data based on the received todoId and update it.
If the id does not exist and cannot be updated, we return a message indicating that.
export const updateTodo = asyncHandler(async (req: Request, res: Response): Promise<void> => {
const todo = await Todo.findByIdAndUpdate(
{ _id: req.params.todoId },
req.body,
{ new: true }
);
if (!todo) {
res.status(404).json({ message: "Todo not found" });
}
res.json(todo)
});
Define routing so that it can receive todoId.
router.route('/:todoId').patch(updateTodo)
Operation check
This time, we will mark an existing Todo as completed.
We will use this Todo as the update target:
[
{
"_id": "678064b4923d0c1c0e9ebea6",
"title": "Reply to inquiries",
"isCompleted": false,
"createdAt": "2025-01-10T00:07:16.292Z",
"__v": 0
},
Set {"isCompleted": true} in the body of the PATCH method.
Set the ID of the update target in the URL.
curl -X PATCH -H "Content-Type: application/json" -d '{"isCompleted": true}' http://localhost:3001/todos/678064b4923d0c1c0e9ebea6
You can confirm in the update result that isCompleted: true.
If you change the id to 678064b4923d0c1c0e9ebea5, changing only the last digit so that it becomes a non-existent id, and send the request, you can confirm that the message you set is returned.
Deleting Data (DELETE method)
We search for the target data based on the received todoId and delete it.
If the id does not exist and cannot be deleted, we return a message indicating that.
export const deleteTodo = asyncHandler(async (req: Request, res: Response): Promise<void> => {
const todo = await Todo.findByIdAndDelete(req.params.todoId);
if (!todo) {
res.status(404).json({ message: "Todo not found" });
}
res.json({ message: "Todo deleted successfully", todo });
});
Define routing for the DELETE method so that it can also receive todoId.
- router.route('/:todoId').patch(updateTodo)
+ router.route('/:todoId').patch(updateTodo).delete(deleteTodo)
Operation check
We will delete this data:
{
"_id": "6780847655833897d0f2f103",
"title": "Post test",
"isCompleted": false,
"createdAt": "2025-01-10T02:22:46.525Z",
"__v": 0
}
]
Send a DELETE request with the following command:
curl -X DELETE http://localhost:3001/todos/6780847655833897d0f2f103
You should be able to confirm that it has been successfully deleted.
If you run it once and then send the same request again, the id will already be non-existent, so a message indicating that it cannot be deleted will be displayed.
Conclusion
In this article, we explained how to build a REST API using MongoDB Atlas, from initial setup to implementation, as well as key points for type-safe development. Through this, you should now have a basic understanding of how to use MongoDB Atlas and TypeScript.
When you further extend your application, you can create more practical and reliable APIs by considering authentication features, advanced error handling, and preparing deployment environments. In addition, by strengthening TypeScript’s typing, you can improve maintainability in team development and long-term projects.
We hope your projects using MongoDB Atlas and TypeScript will be successful.
Recommended Article
This article explains how to efficiently test Express application APIs by combining Supertest and Jest.
It covers introducing a service layer, refactoring toward testable code design, and implementing tests for GET, POST, PATCH, and DELETE methods, with detailed explanations and concrete code examples.
Questions about this article 📝
If you have any questions or feedback about the content, please feel free to contact us.Go to inquiry form
Related Articles
Robust Authorization Design for GraphQL and REST APIs: Best Practices for RBAC, ABAC, and OAuth 2.0
2024/05/13Express (+ TypeScript) Beginner’s Guide: How to Quickly Build Web Applications
2024/12/07Complete Guide to Refactoring React: Improve Your Code with Modularization, Render Optimization, and Design Patterns
2025/01/13Test Automation with Jest and TypeScript: A Complete Guide from Basic Setup to Writing Type-Safe Tests
2023/09/13ESLint / Prettier Introduction Guide: Thorough Explanation from Husky, CI/CD Integration, to Visualizing Code Quality
2024/02/12Practical Microservices Strategy: The Tech Stack Behind BFF, API Management, and Authentication Platform (AWS, Keycloak, gRPC, Kafka)
2024/03/22Building a Mock Server for Frontend Development: A Practical Guide Using @graphql-tools/mock and Faker
2024/12/30Streamlining API Mocking and Testing with Mock Service Worker (MSW)
2023/09/25


















