Explore Developer Center's New Chatbot! MongoDB AI Chatbot can be accessed at the top of your navigation to answer all your MongoDB questions.

Join us at AWS re:Invent 2024! Learn how to use MongoDB for AI use cases.
MongoDB Developer
Atlas
plus
Sign in to follow topics
MongoDB Developer Centerchevron-right
Developer Topicschevron-right
Productschevron-right
Atlaschevron-right

Build a CRUD API With MongoDB, Typescript, Express, Prisma, and Zod

Onyedikachi Kindness Eni9 min read • Published Sep 04, 2024 • Updated Sep 04, 2024
PrismaTypeScriptAtlas
FULL APPLICATION
Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty

Project overview

Traditional technologies often struggle to keep pace with the demands for scalability, maintainability, and rapid development cycles. These systems are plagued by monolithic architectures, difficult-to-maintain codebases, and a lack of modern tooling, leading to inefficiencies and increased development costs.
Enter the modern stack: TypeScript, Express, Prisma, MongoDB, and Zod. This powerful combination offers a robust, type-safe, and scalable solution for building web applications. TypeScript enhances JavaScript with static typing, reducing runtime errors and improving developer productivity. Express, a minimalist web framework, simplifies the creation of APIs with its lightweight yet powerful routing and middleware capabilities. Prisma, a next-generation ORM, provides a type-safe interface to interact with databases, bridging the gap between application logic and database operations. MongoDB Atlas, a leading NoSQL database and data platform, excels in handling large volumes of unstructured data and horizontal scaling. Finally, Zod ensures data integrity with its TypeScript-first schema declaration and validation.
Using this modern stack, we will build a basic user management API that will perform the following CRUD operations:
  • Create user accounts
  • Read user profiles
  • Update user information
  • Delete user accounts
Express will be used to build the web server, Prisma the ORM, MongoDB as the database, and Zod to validate payloads.

Prerequisites

  • Working knowledge of Typescript
  • Familiarity with Express
  • MongoDB Atlas cluster
  • Code editor — such as VS Code
  • Node.js latest LTS installed on your computer
The source code for this project can be found on GitHub.

Project setup

Installing dependencies

Create a project directory called CRUD-API and navigate to it.
1mkdir CRUD-API
2cd CRUD-API
Next, initialize a new project and install Typescript and Prisma:
1npm init -y
2npm install prisma typescript ts-node @types/node --save-dev
The preceding command creates a package.json file with a Typescript setup:
Next, initialize TypeScript:
1 npx tsc --init
Run the following command to invoke Prisma CLI.
1npx prisma
Run the following command to set up your Prisma ORM project:
1npx prisma init
The preceding commanding does the following:
  • Creates a new prisma directory with a schema.prisma file
  • Creates a .env file, where the environment variable used for the database connection will be defined
Run the following command to install the @prisma/client package, which will allow us to use Prisma Client.
1npm install @prisma/client express zod
In the preceding command, we installed the @prisma/client package, express, and zod. The @prisma/client package will allow us to use Prisma Client for database access.
Finally, run the following command:
1npm install @types/express nodemon --save-dev
After installing the dependencies, your package.json file should look like this:
1{
2 "name": "crud-api",
3 "version": "1.0.0",
4 "description": "",
5 "main": "index.js",
6 "scripts": {
7 "test": "echo \"Error: no test specified\" && exit 1"
8 },
9 "keywords": [],
10 "author": "",
11 "license": "ISC",
12 "devDependencies": {
13 "@types/express": "^4.17.21",
14 "@types/node": "^20.12.10",
15 "nodemon": "^3.1.0",
16 "prisma": "^5.13.0",
17 "ts-node": "^10.9.2",
18 "typescript": "^5.4.5"
19 },
20 "dependencies": {
21 "@prisma/client": "^5.13.0",
22 "express": "^4.19.2",
23 "zod": "^3.23.7"
24 }
25}
The tree structure of the project should look like this:
1├── CRUD-API
2│ ├── node_modules
3│ ├── Prisma
4│ │ └── Schema.prisma
5│ ├── src
6│ ├── .gitignore
7│ ├── package.json
8│ ├── package-lock.json
9│ └── tsconfig.json

Setting up Express

In the root directory of the project, create a src sub-directory. The src directory will include the source code of the API.
Next, in the src directory, create an app.ts file and enter the following code:
1import Express from 'express';
2
3// Initiate express
4const app = Express();
5
6app.use(Express.json());
7
8// Setup “hello world” endpoint
9
10const port = process.env.PORT || 3000;
11
12app.get('*', (req, res) => res.send('Hello World!'));
13
14// Start the express server on the relevant port
15app.listen(port, () => {
16 console.log(`server is running on ${port}`);
17});

Running Express

To launch our server, we have to add a new script to our package.json file. Add the following line to the scripts property of the package.json file:
1"dev": "nodemon --watch ./src --ext ts --exec 'ts-node ./src/app.ts'"
In the preceding code, we are using the nodemon command. nodemon is an excellent tool for Node.js applications. It can restart your server when the source changes.
Enter the command below to start the server:
1npm run dev
Now, when you go to your browser and enter http://localhost:3000, you will see the text Hello World! from our Express callback function. HelloWorldHealthCheck

Connecting to the database (MongoDB)

To store our data, we will use MongoDB Atlas. A database connection URL is required to connect the database via Prisma. Follow the guide to set up a MongoDB Atlas cluster and get a connection URL.
Next, in the .env file, add the connection URL as an environment variable:
1DATABASE_URL="mongodb+srv://<username>:<password>@<cluster-name>.xxxxx.mongodb.net/"
In the prisma.schema file, enter the following code:
1generator client {
2 provider = "prisma-client-js"
3}
4
5datasource db {
6 provider = "mongodb"
7 url = env("DATABASE_URL")
8}
The preceding code provides the required data for Prisma to connect to our database. In the datasource block, the value passed to the provider represents the type of database being used (MongoDB, in this case), while the value of url is the database connection URL.

Creating the Prisma schema

Since we are building a basic user management API, only a single collection will be required.

User model

The user model will define the user details to be stored in the database, with the following attributes:
  • name: String, required field to store user’s name
  • email: String, required field to store user’s email
  • phoneNumber: String, required field to store user’s phone number
  • gender: String, required field to store user gender

API endpoints for user CRUD

To enable and handle user CRUD operations on the database, we will implement the following API endpoints.
1Operation API route HTTP method
2Create User /api/users POST
3Get Users /api/users GET
4Fetch a User /api/users:userId GET
5Update a User/api/users:userId PATCH
6Delete a User/api/users:userId DELETE
Now, let’s create the API schema.
In the prisma/schema.prisma file, add the following code:
1model User {
2 id String @id @default(auto()) @map("_id") @db.ObjectId
3 name String
4 email String
5 phoneNumber String
6 gender String
7}
In a traditional relational database, a model will create a table, but since we are using MongoDB, our model creates a collection. Notice the id field is mapped with @map("_id") decorator because _id is the default provided by MongoDB.
Now, let’s generate the Prisma Client by running the following command:
1npx prisma generate
The Prisma Client gives us type-safe access to our database.
Next, we will implement our route controllers which will utilize Prisma Client to perform database operations.

Controllers

Fundamentally, routers and controllers enable the structured handling of requests and responses in an application. The routers map incoming HTTP requests to specific endpoints, while the controllers receive the requests from routers, process any business logic, interact with the database, and return the appropriate responses to the client.
Now, let’s create the routes and controllers.
In the src directory, create a client.ts file and enter the following code:
1import { PrismaClient } from '@prisma/client';
2
3const prisma = new PrismaClient();
4
5export default prisma;
In the preceding code, we created a Prisma Client instance, which will be used for database interaction.
Next, in the src directory, create the routes and controllers sub-directories.
1📦 CRUD-API
2┣ 📂 src
3┃ ┣ 📂 routes
4┃ ┣ 📂 controllers
5┃ ┣ 📄 app.ts
6┃ ┣ 📄 client.ts
By default, a controller handles incoming requests, processes them, and provides an appropriate response. Let’s implement the controllers for our API.
In the controllers subdirectory, create a user.route.ts file.

createUser controller

The createUser handles POST requests for creating new users.
Enter the following code in the user.route.ts file.
1import { Request, Response } from "express";
2import prisma from "../client";
3
4// Creating a user
5export async function createUser(req: Request, res: Response) {
6 try {
7 const user = await prisma.user.create({
8 data: req.body,
9 });
10
11 res.status(201).json({
12 status: true,
13 message: "User Successfully Created",
14 data: user,
15 });
16 } catch (error) {
17 res.status(500).json({
18 status: false,
19 message: 'server error'
20 });
21 }
22}

getUsers controller

The getUser controller handles GET requests for all users.
Enter the following in the user.route.ts file.
1// Get all Users
2export async function getUsers(req: Request, res: Response) {
3 const users = await prisma.user.findMany();
4
5 res.json({
6 status: true,
7 message: "Users Successfully fetched",
8 data: users,
9 });
10}
11
12In the preceding code, the getUsers controller gets all users.
13
14### getUser controller
15The getUser controller handles GET requests for a single user.
16
17Enter the following in the `user.route.ts` file.
18// Get a single user
19export async function getUser(req: Request, res: Response) {
20 const { userid } = req.params;
21 const user = await prisma.user.findFirst({
22 where: {
23 id: userid,
24 },
25 });
26
27 res.json({
28 status: true,
29 message: "User Successfully fetched",
30 data: user,
31 });
32}
In the preceding code, the getUser controller gets a single user.

deleteUser controller

Enter the following in the user.route.ts file.
1// deleting a user
2export async function deleteUser(req: Request, res: Response) {
3 const { userid } = req.params;
4
5 try {
6 const user = await prisma.user.findFirst({
7 where: {
8 id: userid,
9 },
10 });
11
12 if (!user) {
13 return res.status(401).json({
14 status: false,
15 message: 'User not found',
16 });
17 }
18 await prisma.user.delete({
19 where: {
20 id: userid,
21 },
22 }),
23 res.json({
24 status: true,
25 message: 'User Successfully deleted',
26 });
27 } catch {
28 res.status(501).json({
29 status: false,
30 message: 'server error',
31 });
32 }
33}
In the preceding code, the deleteUser controller deletes a user.

updateUser controller

Enter the following in the user.route.ts file.
1// updating a single user
2export async function updateUser(req: Request, res: Response) {
3 try {
4 const { userid } = req.params;
5
6 const user = await prisma.user.findFirst({
7 where: {
8 id: userid,
9 },
10 });
11
12 if (!user) {
13 return res.status(401).json({
14 status: false,
15 message: 'User not found',
16 });
17 }
18
19 const updatedUser = await prisma.user.update({
20 where: {
21 id: userid,
22 },
23 data: req.body,
24 });
25
26 res.json({
27 status: true,
28 message: 'User Successfully updated',
29 data: updatedUser,
30 });
31 } catch (error) {
32 console.log(error);
33 res.status(500).json({
34 status: false,
35 message: 'server error',
36 });
37 }
38}
In the preceding code, the updateUser controller updates a user.
Next, let’s create the routes that will utilize the controllers.

Routes

In the routes subdirectory, create an index.ts and user.routes.ts file.
In index.ts, enter the following code:
1import { Router } from 'express';
2import userRoute from './user.route';
3
4// Index
5const indexRoute = Router();
6
7indexRoute.get('', async (req, res) => {
8 res.json({ message: 'Welcome User' });
9});
10
11indexRoute.use('/users', userRoute);
12
13export default indexRoute;
14
15Next, in user.routes.ts enter the following code:
16import { Router } from 'express';
17import {
18 createUser,
19 deleteUser,
20 getUser,
21 getUsers,
22 updateUser,
23} from '../controllers/user.controller';
24
25// Users layout Route
26const userRoute = Router();
27
28userRoute.post('', createUser);
29userRoute.get('', getUsers);
30userRoute.get('/:userid', getUser);
31userRoute.delete('/:userid', deleteUser);
32userRoute.patch('/:userid', updateUser);
33
34export default userRoute;

Testing with Postman

The GET route can be seen in the browser, but to check the other routes, we will use the POSTMAN tool.

POST

Let’s create a new user via the POST method. In the address bar in Postman, enter the request URL http://localhost:3000/users, enter the payload, and hit the send button. We will receive back the correct JSON and also the status of 201 — confirming a resource has been created. enter image description here POST Request: Create a User

GET

The GET method is used to fetch all users. It requires no payload. enter image description here GET Request: Get all Users
However, to fetch a single user, we have to provide the user id. The endpoint will be http://localhost:3000/users/id.
enter image description here GET Request: Get a single user

DELETE

For the DELETE method, the id of the user being deleted will be provided. Again, we are using the endpoint http://localhost:3000/users/id. enter image description here DELETE: delete a user

PATCH

For the PATCH method also, we have to provide an id of the user being updated. We will use the endpoint http://localhost:3000/users/id. enter image description here PATCH: update a user
Notice we passed an invalid value james.com in the e-mail field and it’s seen as valid. This won’t be the case after we implement the validation middleware.

Validation with Zod

Now, let’s validate the payload of incoming requests (POST, PATCH) to ensure they meet the required data format as specified in our model — i.e., an e-mail must be a valid e-mail address (not just any string).

Validation schema

First, we will create a Zod schema to describe the required data format.
Create a schemas subdirectory in the src directory, and then create a user.schema.tsfile.
Enter the following code in user.schema.ts:
1import { z } from 'zod';
2
3export const createUserSchema = z
4 .object({
5 name: z.string().min(1),
6 email: z.string().email(),
7 phoneNumber: z.string().regex(/^\d+$/),
8 gender: z.enum(['male', 'female', 'others']),
9 })
10 .strict(); //strict prevents the schema from validating payloads with properties not in the schema
11
12export const updateUserSchema = createUserSchema.partial(); //creates a partial schema from createUserSchema where all properties are optional
In the preceding code, the following schemas were created:
  • createUserSchema: This validates the payload coming into the createUser controller via the POST request.
  • updateUserSchema: This validates the payload coming into the updateUser controller.

Validation middleware

Now, let’s create a validation middleware that uses the schema to validate an incoming payload.
1import { NextFunction, Request, Response } from 'express';
2import { ZodSchema } from 'zod';
3
4// Validation middleware
5export const validateSchema =
6 (schema: ZodSchema) => (req: Request, res: Response, next: NextFunction) => {
7 // parse request body
8 const { success, error } = schema.safeParse(req.body);
9
10 // handle non-compliant request body
11 if (!success) {
12 return res.status(401).json({
13 status: false,
14 message: error.errors
15 .map((t) => `${t.path[0] ?? ''}: ${t.message}`)
16 .join(', '),
17 });
18 }
19
20 next();
21 };
In the preceding code, the Zod schema is used to parse the request body. If the parsing is successful, the next function is called. Otherwise, a JSON response with a 401 status code and an error message are returned.

Using the validation middleware

Now, let’s modify the user.routes.ts file by adding the validation middleware to the createUser and updateUser routes.
1import { Router } from 'express';
2import {
3 createUser,
4 deleteUser,
5 getUser,
6 getUsers,
7 updateUser,
8} from '../controllers/user.controller';
9import { validateSchema } from '../middlewares/validation.middleware';
10import { createUserSchema, updateUserSchema } from '../schemas/user.schema';
11
12// Users layout Route
13const userRoute = Router();
14
15userRoute.post('', validateSchema(createUserSchema), createUser);
16userRoute.get('', getUsers);
17userRoute.get('/:userid', getUser);
18userRoute.delete('/:userid', deleteUser);
19userRoute.patch('/:userid', validateSchema(updateUserSchema), updateUser);
20
21export default userRoute;
In comparison to our previous implementation (without validation), sending an invalid payload will return an error now.
Let’s test the PATCH method again: enter image description here PATCH: Invalid e-mail field
This time, we get an error indicating the value of the e-mail field is invalid.
Now, if we use a valid e-mail address, the errors are gone: enter image description here PATCH: Valid e-mail field

Conclusion

In this tutorial, we created a simple CRUD API with Express, MongoDB, and Zod. You learned how to create a server with Express, set up a Prisma schema, validate payloads with Zod, and test API with Postman. Feel free to clone the project from GitHub, sign up for MongoDB Atlas, and build on this foundation. If you want to continue the conversation, join us in the MongoDB Developer Community.

Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
Related
News & Announcements

Unlock the Value of Data in MongoDB Atlas with the Intelligent Analytics of Microsoft Fabric


Nov 17, 2023 | 6 min read
Tutorial

Efficient Sync Solutions: Cluster-to-Cluster Sync and Live Migration to Atlas


May 10, 2024 | 5 min read
Code Example

Final Space API


Jul 07, 2022 | 1 min read
Tutorial

Instant GraphQL APIs for MongoDB with Grafbase


Oct 12, 2023 | 7 min read
Table of Contents