How To Build a REST API with TypeScript, Express and Prisma
The Opinionated Dev
Posted on September 12, 2024
You can find the Github repository here.
After seeing countless videos, articles and news regarding Prisma, a relatively new ORM on the market, I decided it’s time I check it out myself. In this article I’ll give a quick introduction to Prisma, what it does, and we’ll even create a quick REST API with TypeScript, Express, Prisma and SQLite.
What is Prisma?
Prisma is an Object-Relational Mapping tool. It interacts with your database, meaning you don’t have to write SQL queries yourself, making life easier and safer. It also provides type-safe queries and generates your types based on your database schema. It has so many more features and benefits, including: works with PostgreSQL, SQLite, MySQL and even MongoDB.
Setup
Now, it’s time to get started. First let’s init our project and install our dependencies by running these commands:
yarn init -y
yarn add express @prisma/client
yarn add --dev @types/express @types/node nodemon ts-node typescript
Let’s add some scripts to our package.json
file to run our application and other commands we might use often. Here’s mine:
{
"name": "express-prisma-rest-api",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"author": "codeflowjs <codeflowjs@gmail.com>",
"scripts": {
"dev": "nodemon src/index.ts",
"migrate": "prisma migrate dev"
},
"dependencies": {
"@prisma/client": "^4.8.1",
"express": "^4.18.2"
},
"devDependencies": {
"@types/express": "^4.17.15",
"@types/node": "^18.11.18",
"nodemon": "^2.0.20",
"prisma": "^4.8.1",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
}
}
Now let’s create in our root folder a file for our TypeScript config, named tsconfig.json
and here’s the config I use for this tutorial:
{
"compileOnSave": false,
"compilerOptions": {
"target": "es2017",
"lib": [
"es2017",
"esnext.asynciterable"
],
"typeRoots": [
"node_modules/@types"
],
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"module": "commonjs",
"pretty": true,
"sourceMap": true,
"declaration": true,
"outDir": "dist",
"allowJs": true,
"noEmit": false,
"esModuleInterop": true,
"resolveJsonModule": true,
"baseUrl": "src",
},
"include": [
"src/**/*.ts",
],
"exclude": [
"node_modules",
]
}
Database schema
It’s time to create our database schema. In this tutorial, we’ll have 2 models in our database. Author
and Book
. They’ll share a one to many relationship, meaning an Author
can have multiple Books
but a Book
can only have one Author
.
To create this database schema, we’ll have to initialize our Prisma instance, and define the schema.
Run npx prisma init
in your terminal, and you’ll see it generates a new folder called prisma
in the root folder of your application. Inside there, there’s a file called schema.prisma
. All you need to do is paste this into your schema.prisma
file and save it.
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
model Author {
id String @id @default(uuid())
name String
books Book[]
}
model Book {
id String @id @default(uuid())
author Author @relation(fields: [authorId], references: [id])
authorId String
title String
}
To create these models in your database, you need to run yarn migrate
or npx prisma migrate dev
for the changes to be applied.
Creating our server
Now that we are done with all the setting up, let’s get to the coding part. Let’s create a new folder called src
and a file inside called index.ts
. This file will be the heart of our application, being responsible for running our server. The code will look something like this:
import express from "express";
// import authorRouter from "./routes/author.router";
// import bookRouter from "./routes/book.router";
const app = express();
const port = process.env.PORT || 8080;
app.use(express.json());
// app.use("/authors", authorRouter);
// app.use("/books", bookRouter);
app.listen(port, () => {
console.log(`Server listening at http://localhost:${port}`);
});
Next, we’ll create two folder within our src
folder. I’ll call them controllers
and routes
. Controllers will be responsible for connecting to the database and executing CRUD (Create-Read-Update-Delete) operations, while routes will be responsible for invoking the right function based on which endpoint we call.
Creating our authors
In controllers I create a file called authors.controller.ts
to house all logic regarding an author
. Inside this file I’ll have the following code:
import { Request, Response } from "express";
import { Author, PrismaClient } from "@prisma/client";
const authorClient = new PrismaClient().author;
export const getAllAuthors = async (
req: Request,
res: Response
): Promise<void> => {
try {
const allAuthors: Author[] = await authorClient.findMany({ include: { books: true }});
res.status(200).json({ data: allAuthors });
} catch (error) {
console.log(error);
}
};
export const getAuthorById = async (
req: Request,
res: Response
): Promise<void> => {
try {
const authorId = req.params.id;
const author = await authorClient.findUnique({ where: { id: authorId }, include: { books: true } });
res.status(200).json({ data: author });
} catch (error) {
console.log(error);
}
};
export const createAuthor = async (
req: Request,
res: Response
): Promise<void> => {
try {
const authorData = req.body;
const author = await authorClient.create({
data: { ...authorData },
});
res.status(201).json({ data: author });
} catch (error) {
console.log(error);
}
};
export const updateAuthor = async (
req: Request,
res: Response
): Promise<void> => {
try {
const authorId = req.params.id;
const authorData = req.body;
const updatedAuthorData = await authorClient.update({
where: { id: authorId },
data: { ...authorData },
});
res.status(200).json({ data: updatedAuthorData });
} catch (error) {
console.log(error);
}
};
export const deleteAuthor = async (
req: Request,
res: Response
): Promise<void> => {
try {
const authorId = req.params.id;
const authorData = await authorClient.delete({ where: { id: authorId } });
res.status(200).json({ data: {} });
} catch (error) {
console.log(error);
}
};
As you can see, you can fetch a single author, fetch them all, create one, update one or delete one. There’s one important piece however, if you look further in getAuthorById
or getAllAuthors
. Within these two methods, when we interact with the database via authorClient
, you can see that I’m passing in include: { books: true }
as an argument. Because we have a relationship between our models, it means that you can fetch all Authors
, and you can include every Book
in the response that is related to the Author
.
To use these methods, we’ll need to go into our routes
folder and create author.router.ts
. In this file we’ll have all the logic regarding which method gets executed on which request, looking like so:
import { Router } from "express";
import {
getAllAuthors,
getAuthorById,
createAuthor,
updateAuthor,
deleteAuthor,
} from "../controllers/author.controller";
const authorRouter = Router();
authorRouter.get("/", getAllAuthors);
authorRouter.get("/:id", getAuthorById);
authorRouter.post("/", createAuthor);
authorRouter.put("/:id", updateAuthor);
authorRouter.delete("/:id", deleteAuthor);
export default authorRouter;
Creating our books
The next step is to create the same files for the Book
model, so let’s start with creating book.controller.ts
in the controllers
folder. Inside this the code will be almost identical, with one difference:
import { Request, Response } from "express";
import { Book, PrismaClient } from "@prisma/client";
const bookClient = new PrismaClient().book;
export const getAllBooks = async (
req: Request,
res: Response
): Promise<void> => {
try {
const allBooks: Book[] = await bookClient.findMany();
res.status(200).json({ data: allBooks });
} catch (error) {
console.log(error);
}
};
export const getBookById = async (
req: Request,
res: Response
): Promise<void> => {
try {
const bookId = req.params.id;
const book = await bookClient.findUnique({
where: { id: bookId },
});
res.status(200).json({ data: book });
} catch (error) {
console.log(error);
}
};
export const createBook = async (
req: Request,
res: Response
): Promise<void> => {
try {
const bookData = req.body;
console.log(bookData)
const book: Book = await bookClient.create({
data: {
title: bookData.title,
author: {
connect: { id: bookData.authorId }
}
},
});
res.status(201).json({ data: book });
} catch (error) {
console.log(error);
}
};
export const updateBook = async (
req: Request,
res: Response
): Promise<void> => {
try {
const bookId = req.params.id;
const bookData = req.body;
const updatedBookData = await bookClient.update({
where: { id: bookId },
data: { ...bookData },
});
res.status(200).json({ data: updatedBookData });
} catch (error) {
console.log(error);
}
};
export const deleteBook = async (
req: Request,
res: Response
): Promise<void> => {
try {
const bookId = req.params.id;
const book = await bookClient.delete({ where: { id: bookId } });
res.status(200).json({ data: {} });
} catch (error) {
console.log(error);
}
};
If you wish to create a Book
that is connected to an Author
, the key part is to declare it in your createBook
method. As you can see, I’m declaring the book title, and below that there’s one extra field called author with an object inside looking like this: connect: { id: bookData.authorId }
. This is the part that’s crucial if you want your models to be related to each other. You have to connect them. Alternatively you can pass in { authorId: authorId }
and Prisma will do the rest.
With only one item missing, let’s continue by creating book.router.ts
in the routes folder and have this inside:
import { Router } from "express";
import {
getAllBooks,
getBookById,
createBook,
updateBook,
deleteBook,
} from "../controllers/book.controller";
const bookRouter = Router();
bookRouter.get("/", getAllBooks);
bookRouter.get("/:id", getBookById);
bookRouter.post("/", createBook);
bookRouter.put("/:id", updateBook);
bookRouter.delete("/:id", deleteBook);
export default bookRouter;
If you look back in our src/index.ts
file, we have 4 lines commented out. Let’s uncomment them and run the application by typing yarn dev
in the terminal. Now that our server runs, you can execute CRUD operations by using Postman, Insomnia or even spin up a small React app.
I hope you enjoyed this article, here’s the Github repo.
Posted on September 12, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.