Exploring Prisma: Building a Todo App

In the ever-evolving landscape of web development, tools and technologies come and go. However, some innovations stand out for their simplicity, efficiency, and power. One such tool is Prisma, a modern database toolkit that simplifies database access and management, empowering developers to build robust applications with ease. In this blog post, we'll embark on a journey to master Prisma by building a full-fledged Todo application from scratch, leveraging the combined prowess of Prisma, Typescript, and Express. This Blog will more focus on the Prisma ORM and Postgres Database and the way we can use it on our next project smoothly so let's start without any further delay.

What is Prisma?

Prisma is indeed a magical spell for developers! In simple terms, Prisma is an ORM (Object-Relational Mapping) tool that acts as a bridge between your application code and your database. It abstracts away the complexities of writing raw SQL queries for common database operations like Create, Read, Update, and Delete (CRUD). Instead, Prisma provides a clean and intuitive interface for interacting with your database, making the development process smoother and more efficient.

Why Prisma?

Prisma offers several compelling reasons why developers choose it for their projects:

  1. Developer Productivity: Prisma significantly boosts developer productivity by abstracting away the complexities of database management. With its intuitive API and automatic query generation, developers can focus more on building features and less on writing repetitive SQL code.

  2. Type-Safe Database Access: Prisma provides type-safe database access, meaning developers can leverage the type system of their programming language (such as TypeScript) to catch errors at compile time rather than runtime. This helps in preventing common bugs and ensures more robust code.

  3. Declarative Data Modeling: Prisma allows developers to define their data models using a declarative syntax, making it easy to visualize and maintain the database schema. This approach simplifies database design and promotes consistency across the application.

Project Setup

Let's set the project in our local system using the following command, before just a small disclaimer, this blog will mainly go on the backend side especially performing CRUD operations with SQL database with the help of Prisma as ORM.

Steps to Follow:

Set up the Empty Node App with Typescript in your local project directory. Also, install other dependencies like express cors prisma types for node.

npm init -y
npm i  prisma typescript ts-node @types/node --save-dev

Create a tsconfig.json file that helps to handle the typescript to javascript conversion correctly in the project.Use the below code to do this

npx tsc --init

We will write our .ts file in src folder and we want that after the compile we want that respective js file in the dist folder, to achieve this we will do simple changes in the tsconfig.json file as follows

Change `rootDit` to `src`
Change `outDir` to `dist`

Create a Fresh Prisma Project inside the Project folder

npx prisma init

Create a Fresh PostgresDB instance in the Cloud using Neon or Aiven and get the connection string.It is easy to setup both of them

If it is your first time with Prisma then try to get this Prisma Extension from the marketplace in VsCode.Help you to interact better with Prisma.

Now Add these connection strings to the schema.prisma file inside your prisma folder.

Now let's make the Schema for our Users and Todo in the same schema.prisma file.

model User {
  id         Int      @id @default(autoincrement())
  username   String   @unique
  password   String
  email     String   @unique
  todos      Todo[] 
    @@map("User")
}

model Todo {
  id          Int      @id @default(autoincrement())
  userId      Int      
  title       String
  description String
  user        User     @relation(fields: [userId], references: [id]) 

  @@map("Todo")

}

Migration is the process of converting high-level schema definitions from Prisma into low-level SQL queries understood by databases. Prisma abstracts SQL complexities, providing schema definitions describing data models. Changes made in Prisma's schema trigger the automatic generation of SQL migration scripts. These scripts contain commands to create, modify, or delete tables, columns, indexes, and constraints in the database. Prisma's migration tool applies these scripts to the database, ensuring it reflects the updated schema. Migration simplifies database management, maintaining consistency between Prisma's schema and the actual database structure.So let's migrate our Prisma code.

npx prisma migrate dev

Now we have to create Auto-generated Clients which are nothing but they are the function which converts this simple syntax to a SQL query.

npx prisma generate

Now let's go with our Prisma client code to create a user, todo, and get the todos from the database.

//db.ts inside src folder
import { PrismaClient, Todo } from '@prisma/client';

const prisma = new PrismaClient();

export const createUser = async (username: string, password: string, email: string) => {
  return await prisma.user.create({
    data: {
      username,
      password,
      email,
    },
  });
};

export const createTodo = async (userId:number,title: string, description: string) => {
  return await prisma.todo.create({
    data: {
      title,
      description,
      user: { connect: { id: userId } }
    },
  });
};

export const getAllTodos = async (username: string) => {
  const user = await prisma.user.findUnique({
    where: {
      username: username
    },
    include: {
      todos: true 
    }
  });

  if (!user) {
    throw new Error(`User with username ${username} not found`);
  }

  return user.todos;
};


export const updateTodo = async (todoId: number, data: Partial<Todo>) => {
  return await prisma.todo.update({
    where: { id: todoId },
    data,
  });
};


export const deleteTodo = async (todoId: number) => {
  return await prisma.todo.delete({
    where: { id: todoId },
  });
};

export default prisma;

Also let's create some basic basic CRUD Apis for our Todo App in our index.ts file

npm i @types/cors @types/express @types/jsonwebtoken
import express, { Request, Response } from "express";
import { createUser, updateTodo, createTodo } from "./db";
import jwt from "jsonwebtoken";
import cors from 'cors';
const app = express();
const PORT = 3000;

app.use(express.json());
app.use(cors());

app.get("/", (req: Request, res: Response) => {
  res.send("Hello World!");
});

app.post("/signup", async (req: Request, res: Response) => {
  try {
    const { username, email, password } = req.body;
    const newUser = await createUser(username, email, password);
    const token = jwt.sign({ username: username, userId: newUser.id },"#1234astra");

    res
      .status(200)
      .json({ message: "User Created Successfully!!!", token: token });
  } 
  catch (error) {
    console.error("Error signing up:", error);
    res.status(500).json({ message: "Error signing up" });
  }
});

app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

Now we can add some more APIs for creating todo, get to do for respective users, delete the todo, and also update them.


app.post ("/login", async (req: Request, res: Response) => {
  try {
    const {email,password} = req.body;
    const User=await checkUser(email, password);

    if(!User){
      res.status(401).send("UnAuthorized !!!");
    }
    const token = jwt.sign({ email: email, userId: User?.id },"#1234astra");
    res.status(200).json({token:token, message: "Login Succesfully!!!" });


  } 
  catch (error) {
    console.error(error);
    res.send(500).send("Internal Server Error");
  }  


});

app.post("/createTodo", async (req: Request, res: Response) => {
  try {
    const { title, description } = req.body;
    const token = req.headers.authorization?.split(" ")[1];
    if (token) {
      const decodeToken = jwt.decode(token) as JwtPayload | null;
      const userId = decodeToken?.userId;
      const newTodo = await createTodo(userId, title, description);
      if (newTodo) {
        res.status(200).json({ data: newTodo, message: "New Todo Created Successfully!!!" });
      } else {
        res.status(409).send("Bad Request");
      }
    } else {
      res.status(401).send("Unauthorized: Token not provided");
    }
  } catch (error) {
    console.error(error);
    res.status(500).send("Internal Server Error");
  }
});

app.patch("/updateTodo/:id", async (req: Request, res: Response) => {
  try {
    const { id } = req.params;
    const { title, description } = req.body;
    await updateTodo(parseInt(id), { title, description });
    res.status(200).send("Todo Updated Successfully!!!");
  } catch (error) {
    console.error(error);
    res.status(500).send("Internal Server Error");
  }
});

app.get("/getTodos", async (req: Request, res: Response) => {
  try {
    const token = req.headers.authorization?.split(" ")[1];
    if (token) {
      const decodeToken = jwt.decode(token) as JwtPayload | null;
      const email = decodeToken?.email;
      const allTodos = await getAllTodos(email); 
      if (allTodos && allTodos.length > 0) { 
        res.status(200).json({ data: allTodos, message: "All Todos" });
        return; 
      }
    }
    res.status(404).send("No Todos Found !!!");
  } catch (error) {
    console.error(error);
    res.status(500).send("Internal Server Error");
  }
});

app.delete("/deleteTodo/:id",async(req:Request,res:Response)=>{
  try {
    debugger;
    const todo_id=parseInt(req.params.id);
    console.log(todo_id);
   const deleted= await deleteTodo(todo_id);
    if(deleted){
      res.status(200).send("Todo deleted successfully!!!");
    }
  } 
  catch (error) {
    console.error(error);
    res.send(500).send("Internal Server Error");

  }
});

I know there are lot more we can do with this code, like we can write good code with more validation using Zod and we can also introduce middleware to ensure DRY principle.

So as we are at the end of our backend we have to compile the typescript and test our API in Postman. I hope you know how to Test the APIs in Postman.

So run this command to compile the typescript to javascript and as we mentioned above that our compiled file will we in the ./dist folder.

tsc -b

So now we can run our server by writing the given command in dist folder or we can also use nodemon to restart the server every time we save the code.

node index.js

Now Let's quickly check the APIs whether they are working or not on Postman.

Signup

Login

Create Todo

Get Todos

Update Todo

Delete Todo

Things to Remember:

  1. Generate Prisma Client: After making any changes to your Prisma schema (schema.prisma), it's essential to generate the Prisma Client. This client is responsible for interacting with your database and reflects the changes made in your schema. You can generate the Prisma Client by running npx prisma generate in your terminal.

  2. Migrate Database Changes: If your schema changes require modifications to the database structure, you'll need to generate and apply migrations using Prisma Migrate. Running npx prisma migrate dev will automatically generate migration files based on the changes in your schema and apply them to your database.

  3. Update Prisma Client Usage: After generating the Prisma Client, ensure that you update your application code to reflect any changes in the Prisma Client API. For example, if you add new models or fields to your schema, you'll need to update your queries and mutations accordingly in your application code.

  4. Test Changes: After updating your schema and application code, it's crucial to thoroughly test your application to ensure that everything works as expected. Test both existing functionality and any new features or changes introduced.

  5. Review Performance: Depending on the nature of your changes, it's essential to review the performance implications. For example, adding new indexes or optimizing queries may be necessary to maintain or improve performance, especially for large datasets or frequently accessed data.

    I hope you learned something valuable about Prisma from this blog. If so, please consider sharing it with your friends and colleagues. Don't forget to tag me on X so I can see your post! Thank you for your support!

Did you find this article valuable?

Support Vishal Sharma by becoming a sponsor. Any amount is appreciated!