How To Build a REST API with TypeScript, Express and Prisma

theopinionateddev

The Opinionated Dev

Posted on September 12, 2024

How To Build a REST API with TypeScript, Express and Prisma

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
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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",
  ]
}
Enter fullscreen mode Exit fullscreen mode

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.

Database relationship between Author and Book

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
}
Enter fullscreen mode Exit fullscreen mode

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}`);
});
Enter fullscreen mode Exit fullscreen mode

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
  }
};
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
theopinionateddev
The Opinionated Dev

Posted on September 12, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related