Build a REST API with Prisma, Node JS and Typescript.
Joshua Olajide
Posted on March 11, 2023
Introduction
The process of learning a new technology can often feel tedious, especially in the initial stages when you have yet to see it in action.
This article will guide you through the process of integrating these amazing technologies and demonstrate how to build a small application using them.
During the course of this article, we will develop a blog application that showcases how CRUD operations work in practice, with fundamental functions including:
- Creating new posts
- Retrieving all posts or a specific post
- Modifying post content
- And ultimately, removing a post
- An additional feature we will implement is the inclusion of a like count for each post.
CRUD means CREATE, READ, UPDATE and DELETE
If you are interested in checking out the code, Check out the github repository here
Prerequisite
Before you proceed, it's important to be acquainted with the following set of tools:
- Node.js and Express.js
- TypeScript
- Prisma
If you require a brief overview of Prisma, my blog post here can be a helpful resource.
Let's Get Started π
Open up your terminal then create and navigate to your folder directory using the below command
mkdir prisma-typescript-blog && cd prisma-typescript-blog
Next, Initialize the project using yarn
yarn init -y
Then, Install the Dependencies
yarn add -D @types/express @types/node prisma ts-node-dev typescript
yarn add express @prisma/client
Finally, set up Prisma with the init
command of the Prisma CLI:
npx prisma init --datasource-provider sqlite
This creates a new prisma directory with your Prisma schema file and configures Sqlite as your database.
Set up Prisma Schema
prisma/prisma.schema
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Post {
id Int @id @default(autoincrement())
title String
content String
likesCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
comments Comment[]
}
model Comment {
id Int @id @default(autoincrement())
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
post Post @relation(fields: [postId], references: [id])
postId Int
}
In the above schema we define our Blog Models which includes Post
and Comment
post Post @relation(fields: [postId], references: [id])
This line of code means that there's a one-to-many relationship between the Post
and Comment
models. One post can have many comments, and each comment is associated with only one post. The postId
field in the Comment
model is used to reference the id
field in the Post
model.
Once you are done with this, Update your package.json
to look like this:
{
"name": "prisma-typescript-blog",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"start": "ts-node-dev --respawn --transpile-only --exit-child src/server.ts",
"db:migrate": "npx prisma migrate dev --name user-entity --create-only && npx prisma generate",
"db:push": "npx prisma db push"
},
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": [
"es6",
"dom"
]
},
"devDependencies": {
"@types/express": "^4.17.17",
"prisma": "^4.11.0",
"ts-node-dev": "^2.0.0",
"typescript": "^4.9.5"
},
"dependencies": {
"@prisma/client": "^4.11.0",
"dotenv": "^16.0.3",
"express": "^4.18.2"
}
}
-
start
Starts the Node Js Server -
db:migrate
- Generate the migration file and Prisma Client -
db:push
- Pushes the Prisma schema to the database
Migration
At this point, you have a Prisma model but no database yet. Open your terminal and run the following command to create the SQLite database and the βpostsβ table represented by the model.
yarn db:migrate
After running the command, the Prisma CLI will analyze your schema and generate a migration file in the prisma/migrations
directory.
Push the Prisma schema to the database:
yarn db:push
After synching the SQLite database with the Prisma schema, run npx prisma studio
to open the Prisma GUI tool in the browser. Prisma studio allows you to view and mutate the data stored in the database.
By now, Your browser should automatically open a new tab and you will see something like this:
Project Structure π§
Edit your project structure to look like below
`prisma` -|
`schema.prisma`
`src` -|
`controllers`
-|
`post.controller.ts`
`routes`
-|
`post.route.ts`
`server.ts`
.env
`tsconfig.json`
.env
DATABASE_URL="file:./dev.db"
Replace whatever is in your .env
file with the above snippet
Let's move on to the fun part π
server.ts
import express, { Request, Response } from "express";
import { PrismaClient } from "@prisma/client";
import PostRouter from "./routes/blog.route";
export const prisma = new PrismaClient();
const app = express();
const port = 8080;
async function main() {
app.use(express.json());
// Register API routes
app.use("/api/v1/post", PostRouter);
// Catch unregistered routes
app.all("*", (req: Request, res: Response) => {
res.status(404).json({ error: `Route ${req.originalUrl} not found` });
});
app.listen(port, () => {
console.log(`Server is listening on port ${port}`);
});
}
main()
.then(async () => {
await prisma.$connect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});
Quite a lot is going on here, so let me explain
-
prisma = new PrismaClient()
- Creates a new instance of the Prisma client - The
express.json()
Registers middleware to parse incoming request bodies as JSON. -
app.use("/api/v1/post", PostRouter);
Registers API routes using the PostRouter module. -
app.all("*",...
Defines a catch-all route to handle unregistered routes and return a 404 error response.
When the main()
function is called, it starts the Express server and connects to the database using the Prisma client. If there are any errors, it logs the error to the console and disconnects from the database.
The code serves as a starting point for building an API server with Node.js, Express.js, and Prisma ORM. It demonstrates how to handle API routes, connect to a database, and start a server that listens for incoming requests.
Routes
routes/post.route.ts
import express from "express";
import PostController from "../controllers/post.controller";
const router = express.Router();
router.post("/create", PostController.createBlogPost);
router.post("/createPostAndComments", PostController.createPostAndComments);
router.get("/getall", PostController.getBlogPosts);
router.get("/get/:id", PostController.getBlogPost);
router.put("/update/:id", PostController.updateBlogPost);
router.delete("/delete/:id", PostController.deleteBlogPost);
router.delete("/deleteall", PostController.deleteAllBlogPosts);
router.post("/like", PostController.likeBlogPost);
export default router;
These routes are pretty straight forward except for the /createPostAndComments
route. Each route handles a specific HTTP method (POST, GET, PUT, or DELETE). The routes are mapped to methods defined in the PostController
controller, which handles the logic for each of these routes.
The
/:id
portion of these routes is a parameter that represents a dynamic value that can be passed in the URL. Theid
parameter is a placeholder for a specific value, such as a unique identifier for a blog post.The
createPostAndComments
will help us demonstrate a one to many relationship as it was defined in the Schema earlier.
In the Post
Prisma model, there is a one-to-many relationship between Post
and Comment
, where one Post
can have many Comments
, but each Comment
can only belong to one Post
. This relationship is modeled in the Prisma schema using the comments
field in the Post
model:
model Post {
id Int @id @default(autoincrement())
title String
content String
likesCount Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relations
comments Comment[]
}
As you can see, the comments
field is an array of Comment
objects, which represents the one-to-many relationship.
Controllers
controllers/post.controller.ts
import { Request, Response } from "express";
import { prisma } from "../server";
const createBlogPost = async (req: Request, res: Response) => {
try {
const { title, content } = req.body;
const newBlogPost = await prisma.post.create({
data: {
title,
content,
},
});
res.status(200).json(newBlogPost);
} catch (e) {
res.status(500).json({ error: e });
}
};
const createPostAndComments = async (req: Request, res: Response) => {
try {
const { title, content, comments } = req.body;
const newBlogPost = await prisma.post.create({
data: {
title,
content,
comments: {
create: comments,
},
},
include: {
comments: true, // Include the comments in the response
}
});
res.status(200).json(newBlogPost);
} catch (e) {
res.status(500).json({ error: e });
}
};
const getBlogPosts = async (req: Request, res: Response) => {
try {
const blogPosts = await prisma.post.findMany();
res.status(200).json(blogPosts);
} catch (e) {
res.status(500).json({ error: e });
}
};
const getBlogPost = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const blogPost = await prisma.post.findUnique({
where: {
id: Number(id),
},
});
res.status(200).json(blogPost);
} catch (e) {
res.status(500).json({ error: e });
}
};
const updateBlogPost = async (req: Request, res: Response) => {
try {
const { id, title, content } = req.body;
const updatedBlogPost = await prisma.post.update({
where: {
id: Number(id),
},
data: {
title,
content,
},
});
res.status(200).json(updatedBlogPost);
} catch (e) {
res.status(500).json({ error: e });
}
};
const deleteBlogPost = async (req: Request, res: Response) => {
try {
const { id } = req.body;
const deletedBlogPost = await prisma.post.delete({
where: {
id: Number(id),
},
});
res.status(200).json(deletedBlogPost);
} catch (e) {
res.status(500).json({ error: e });
}
};
const deleteAllBlogPosts = async (req: Request, res: Response) => {
try {
const deletedBlogPosts = await prisma.post.deleteMany();
res.status(200).json(deletedBlogPosts);
} catch (e) {
res.status(500).json({ error: e });
}
};
const likeBlogPost = async (req: Request, res: Response) => {
try {
const { id } = req.body;
const likedBlogPost = await prisma.post.update({
where: {
id: Number(id),
},
data: {
likesCount: {
increment: 1,
},
},
});
res.status(200).json(likedBlogPost);
} catch (e) {
res.status(500).json({ error: e });
}
};
export default {
createBlogPost,
createPostAndComments,
getBlogPosts,
getBlogPost,
updateBlogPost,
deleteBlogPost,
deleteAllBlogPosts,
likeBlogPost,
};
This controller is quite bulky, so I will go ahead and explain each method one after the other.
Our methods are listed as follows
createBlogPost
createPostAndComments
getBlogPosts
getBlogPost
updateBlogPost
deleteBlogPost
deleteAllBlogPosts
likeBlogPost
createBlogPost
This is a pretty simple method that accepts the title
and content
. It first destructures the title and content values from the req.body
object, which is the request payload.
Also, it uses the create()
method to create a new blog post in the database. This method returns a Promise, which is why the await keyword is used to wait for the operation to complete before continuing execution.
When a request is made to the
http://localhost:8080/api/v1/post/create
endpoint with the credentials included in the request body, thecreateBlogPost
handler will be evoked to create a new blog post.
Postman is my preferred tool for testing all the endpoints, but feel free to use whichever testing tool you prefer.
createPostAndComments
The createPostAndComments method expects the request body to include a title
, content
, and an array of comments
. The method then creates the new Post object using the create() method provided by Prisma, and includes the comments
in the data object as well.
The Comment
object will include the content
field, as the postId
field will be automatically set by Prisma when the Comment
is associated with the new Post
.
Finally, the include
parameter is used to include the comments
for the newly created Post
in the response.
Here is how the payload and response looks like
updateBlogPost
This method first extracts the id
, title
, and content
properties from the request body using object destructuring.
It then uses Prisma to update the blog post with the specified id
. The update()
method is called on the post
model, passing in an object that specifies the where
and data
clauses. The where
clause specifies the unique identifier for the blog post to be updated, while the data
clause specifies the new values for the title
and content
properties.
likeBlogPost
This method first extracts the id
property from the request body. This id
represents the unique identifier of the blog post to be liked.
It then uses Prisma prisma.post.update()
to update the specified blog post. The update()
method is called on the post
model, passing in an object that specifies the where
and data
clauses. The where
clause specifies the unique identifier for the blog post to be updated, while the data clause increments
the likesCount
property by 1
using the increment keyword
Here is it in action
When a request is made to the
http://localhost:8080/api/v1/post/like
endpoint passingid
as body, It then invokes thecreateBlogPost
handler andlikesCount
will be incremented
All other methods are quite similar to what has been covered so I won't be going over them
After all these steps, Be sure your server is still running if not run yarn start
to get it up and running.
In Conclusion
This article demonstrates how to apply your knowledge of this amazing tools by creating a blog API using Node, Typescript, and Prisma. Additionally, the tutorial covers the process of connecting the API with a SQLite database.
Congratulations on reaching the end of this article! If you found it helpful, please consider giving it a thumbs up and leaving your comments below.
See you in the next ππ
Posted on March 11, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.