← Back to Blog

Backend with TypeScript, PostgreSQL & Prisma: Data Modeling & CRUD

Daniel Norman
Daniel Norman
July 30, 2020
Updated July 3, 2026

Update (July 2026): This article was updated for Prisma ORM 7. It uses the rust-free prisma-client generator, the prisma.config.ts config file, and driver adapters. All commands and code work verbatim on Prisma ORM 7 with Node.js 20 or later.

This article is part of a series on building a backend with TypeScript, PostgreSQL, and Prisma. In this first part, you will design a data model, apply it to your database with Prisma Migrate, and perform CRUD and aggregate queries with Prisma Client.

Introduction

The goal of the series is to explore and demonstrate different patterns, problems, and architectures for a modern backend by solving a concrete problem: a grading system for online courses. This is a good example because it features diverse relation types and is complex enough to represent a real-world use case.

What the series covers

The series focuses on the role of the database in every aspect of backend development, covering:

  • Data modeling
  • CRUD
  • Aggregations
  • API layer
  • Validation
  • Testing
  • Authentication
  • Authorization
  • Integration with external APIs
  • Deployment

What you will learn today

This first article lays out the problem domain and develops the following aspects of the backend:

  1. Data modeling: Mapping the problem domain to a database schema
  2. CRUD: Implementing Create, Read, Update, and Delete queries with Prisma Client
  3. Aggregation: Implementing aggregate queries with Prisma Client to calculate averages and more

By the end of this article you will have a Prisma schema, a corresponding database schema created by Prisma Migrate, and a seed script that uses Prisma Client to perform CRUD and aggregation queries.

Note: Throughout the guide you'll find various checkpoints that enable you to validate whether you performed the steps correctly.

Prerequisites

Assumed knowledge

This series assumes basic knowledge of TypeScript, Node.js, and relational databases. If you're experienced with JavaScript but haven't had the chance to try TypeScript, you should still be able to follow along. The series uses PostgreSQL, however, most of the concepts apply to other relational databases such as MySQL. Beyond that, no prior knowledge of Prisma is required.

Development environment

You should have the following installed:

If you're using Visual Studio Code, the Prisma extension is recommended for syntax highlighting, formatting, and autocomplete in your schema.

You will create a managed Prisma Postgres database from the terminal during setup, so you don't need a local database server. If you prefer to run PostgreSQL locally, a Docker alternative is included in the database step.

Create the project

Create a new directory and initialize a TypeScript project:

mkdir grading-app
cd grading-app
npm init -y
npm install typescript tsx @types/node --save-dev

Install Prisma ORM and the packages it needs to talk to PostgreSQL:

npm install prisma @types/pg --save-dev
npm install @prisma/client @prisma/adapter-pg pg dotenv

Here is what each package does:

  • prisma: the Prisma CLI for running commands like prisma init, prisma migrate, and prisma generate
  • @prisma/client: the Prisma Client library for querying your database
  • @prisma/adapter-pg: the driver adapter that connects Prisma Client to PostgreSQL through node-postgres
  • pg: the node-postgres database driver
  • @types/pg: TypeScript type definitions for node-postgres
  • dotenv: loads environment variables from your .env file

Prisma ORM 7 is ESM-first. Update your tsconfig.json for ESM compatibility:

{
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "bundler",
    "target": "ES2023",
    "strict": true,
    "esModuleInterop": true
  }
}

And enable ESM in your package.json:

{
  "type": "module"
}

Create your database

This tutorial uses Prisma Postgres, a managed PostgreSQL database that you can provision straight from the terminal. Create one with:

npx create-db

The command provisions a new database and prints a postgres://... connection string. Keep that string handy. You will add it to your .env file when you initialize Prisma ORM, and you can claim the database in the Prisma Console to keep it.

Prisma Postgres speaks the standard PostgreSQL protocol, so everything in this tutorial works identically against any other PostgreSQL database.

Alternative: run PostgreSQL locally with Docker

If you would rather run the database locally, create a docker-compose.yml file in the project root:

services:
  postgres:
    image: postgres:17
    restart: always
    environment:
      POSTGRES_USER: prisma
      POSTGRES_PASSWORD: prisma
      POSTGRES_DB: grading-app
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Start the database:

docker compose up -d

Initialize Prisma ORM

Set up your Prisma ORM project with the following command:

npx prisma init --output ../generated/prisma

This command does three things:

  • Creates a prisma/ directory with a schema.prisma file for your data model
  • Creates a .env file for environment variables
  • Creates a prisma.config.ts file for project configuration

The prisma.config.ts file is the single place where you configure how Prisma interacts with your project: schema location, migrations, seed scripts, and the database connection. Because it is a TypeScript file, you can load values dynamically, for example with dotenv. The generated file looks like this:

import "dotenv/config";
import { defineConfig } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
  },
  datasource: {
    url: process.env["DATABASE_URL"],
  },
});

The generated prisma/schema.prisma uses the prisma-client generator. This generator produces a rust-free, ESM-compatible client and writes the generated code into your project source instead of node_modules, so your build tools and file watchers treat it like any other part of your app:

generator client {
  provider = "prisma-client"
  output   = "../generated/prisma"
}

datasource db {
  provider = "postgresql"
}

Finally, set the connection string in .env. Use the postgres://... URL that npx create-db printed when you created your database:

DATABASE_URL="postgres://<your-connection-string-from-create-db>"

If you chose the Docker alternative, use the local connection string instead:

DATABASE_URL="postgresql://prisma:prisma@localhost:5432/grading-app"

Note: It's considered best practice to keep secrets out of your codebase. The connection URL is loaded from the environment by prisma.config.ts, so the schema file contains no credentials.

Data model for a grading system for online courses

Defining the problem domain and entities

When building a backend, one of the foremost concerns is a proper understanding of the problem domain. The problem domain (or problem space) refers to all the information that defines the problem and constrains the solution. By understanding the problem domain, the shape and structure of the data model becomes clear.

The online grading system has the following entities:

  • User: A person with an account. A user can be either a teacher or a student through their relation to a course. In other words, the same user who's a teacher of one course can be a student in another course.
  • Course: A learning course with one or more teachers and students, as well as one or more tests. For example: an "Introduction to TypeScript" course can have two teachers and ten students.
  • Test: A course can have many tests to evaluate the students' comprehension. Tests have a date and are related to a course.
  • Test result: Each test can have multiple test result records per student. Additionally, a TestResult is also related to the teacher who graded the test.

Note: An entity represents either a physical object or an intangible concept. For example, a user represents a person, whereas a course is an intangible concept.

The entities can be visualized to demonstrate how they would be represented in a relational database (in this case PostgreSQL). The diagram below adds the columns relevant for each entity and foreign keys to describe the relationships between the entities.

The first thing to note about the diagram is that every entity maps to a database table.

The diagram has the following relations:

  • one-to-many (also known as 1-n):
    • TestTestResult
    • CourseTest
    • UserTestResult (via graderId)
    • UserTestResult (via studentId)
  • many-to-many (also known as m-n):
    • UserCourse via the CourseEnrollment relation table with two foreign keys: userId and courseId. Many-to-many relations typically require an additional table. This is necessary so that the grading system can have the following properties:
      • A single course can have many associated users (as students or teachers)
      • A single user can be associated with many courses.

Note: A relation table (also known as a JOIN table) connects two or more other tables to create a relation between them. Creating relation tables is a common data modeling practice in SQL to represent relationships between different entities. In essence, it means that "one m-n relation is modeled as two 1-n relations in the database".

Understanding the Prisma schema

To create the tables in your database, you first define your Prisma schema. The Prisma schema is a declarative definition of your database tables. It serves as the source of truth for both the generated Prisma Client and for Prisma Migrate, which creates the database schema.

Define models

The fundamental building block of the Prisma schema is the model. Every model maps to a database table.

Here is an example showing the basic signature of a model:

model User {
  id        Int     @id @default(autoincrement())
  email     String  @unique
  firstName String
  lastName  String
  social    Json?
}

Each field has a name followed by a type and optional field attributes. The User model breaks down as follows:

NameTypeScalar vs RelationType modifierAttributes
idIntScalar-@id (primary key), @default(autoincrement())
emailStringScalar-@unique
firstNameStringScalar--
lastNameStringScalar--
socialJsonScalar? (optional)-

Prisma defines a set of data types that map to native database types depending on the database used.

The Json type stores free-form JSON. This is useful for information that can vary across User records and change without affecting the core functionality of the backend. In the User model it stores social links, for example a Bluesky or LinkedIn handle. Adding a new social profile link requires no database migration.

With a good understanding of the problem domain, add the following models to your prisma/schema.prisma file:

model User {
  id        Int     @id @default(autoincrement())
  email     String  @unique
  firstName String
  lastName  String
  social    Json?
}

model Course {
  id            Int     @id @default(autoincrement())
  name          String
  courseDetails String?
}

model Test {
  id        Int      @id @default(autoincrement())
  updatedAt DateTime @updatedAt
  name      String // Name of the test
  date      DateTime // Date of the test
}

model TestResult {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  result    Int // Percentage precise to one decimal point represented as `result * 10^-1`
}

Each model has all the relevant fields while ignoring relations, which come next.

Define relations

One-to-many

To define a one-to-many relation between Test and TestResult, add the following three fields:

  • A testId field of type Int (relation scalar) on the "many" side of the relation, TestResult. This field represents the foreign key in the underlying database table.
  • A test field of type Test (relation field) with a @relation attribute mapping the relation scalar testId to the id primary key of the Test model.
  • A testResults field of type TestResult[] (relation field) on Test.
model Test {
  id        Int      @id @default(autoincrement())
  updatedAt DateTime @updatedAt
  name      String
  date      DateTime

  testResults TestResult[] // relation field
}

model TestResult {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  result    Int

  testId Int // relation scalar field
  test   Test @relation(fields: [testId], references: [id]) // relation field
}

Relation fields like test and testResults can be identified by their value type pointing to another model. Their names affect how relations are accessed programmatically with Prisma Client, but they don't represent real database columns.

Many-to-many

Many-to-many relations can be implicit or explicit in the Prisma schema.

To create an implicit many-to-many relation between User and Course, you would define relation fields as lists on both sides:

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  firstName String
  lastName  String
  social    Json?
  courses   Course[]
}

model Course {
  id            Int     @id @default(autoincrement())
  name          String
  courseDetails String?
  members       User[]
}

With this, Prisma manages the relation table for you. However, one of the requirements of the grading system is to relate users to a course with a role, as either a teacher or a student. That means storing meta-information about the relation in the database.

This is what explicit many-to-many relations are for. With an explicit relation, you define the relation table as its own model and can add extra fields to it. Define a CourseEnrollment model and point the courses field on User and the members field on Course at it:

model User {
  id        Int                @id @default(autoincrement())
  email     String             @unique
  firstName String
  lastName  String
  social    Json?
  courses   CourseEnrollment[]
}

model Course {
  id            Int                @id @default(autoincrement())
  name          String
  courseDetails String?
  members       CourseEnrollment[]
}

model CourseEnrollment {
  createdAt DateTime @default(now())
  role      UserRole

  // Relation fields
  userId   Int
  user     User   @relation(fields: [userId], references: [id])
  courseId Int
  course   Course @relation(fields: [courseId], references: [id])

  @@id([userId, courseId])
  @@index([userId, role])
}

enum UserRole {
  STUDENT
  TEACHER
}

Things to note about the CourseEnrollment model:

  • It uses the UserRole enum to denote whether a user is a student or a teacher of a course.
  • @@id([userId, courseId]) defines a multi-field primary key of the two fields. This ensures that every User can only be associated with a Course once, either as a student or as a teacher but never both.

To learn more about relations, check out the relations documentation.

The full schema

Now that you've seen how relations are defined, update prisma/schema.prisma with the complete data model:

generator client {
  provider = "prisma-client"
  output   = "../generated/prisma"
}

datasource db {
  provider = "postgresql"
}

model User {
  id        Int     @id @default(autoincrement())
  email     String  @unique
  firstName String
  lastName  String
  social    Json?

  // Relation fields
  courses     CourseEnrollment[]
  testResults TestResult[]       @relation(name: "results")
  testsGraded TestResult[]       @relation(name: "graded")
}

model Course {
  id            Int     @id @default(autoincrement())
  name          String
  courseDetails String?

  // Relation fields
  members CourseEnrollment[]
  tests   Test[]
}

model CourseEnrollment {
  createdAt DateTime @default(now())
  role      UserRole

  // Relation fields
  userId   Int
  courseId Int
  user     User   @relation(fields: [userId], references: [id])
  course   Course @relation(fields: [courseId], references: [id])

  @@id([userId, courseId])
  @@index([userId, role])
}

model Test {
  id        Int      @id @default(autoincrement())
  updatedAt DateTime @updatedAt
  name      String
  date      DateTime

  // Relation fields
  courseId    Int
  course      Course       @relation(fields: [courseId], references: [id])
  testResults TestResult[]
}

model TestResult {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  result    Int // Percentage precise to one decimal point represented as `result * 10^-1`

  // Relation fields
  studentId Int
  student   User @relation(name: "results", fields: [studentId], references: [id])
  graderId  Int
  gradedBy  User @relation(name: "graded", fields: [graderId], references: [id])
  testId    Int
  test      Test @relation(fields: [testId], references: [id])
}

enum UserRole {
  STUDENT
  TEACHER
}

Note that TestResult has two relations to the User model: student and gradedBy, representing both the student who took the test and the teacher who graded it. The name argument on the @relation attribute disambiguates the relations when a single model has more than one relation to the same model.

Migrating the database

With the Prisma schema defined, use Prisma Migrate to create the actual tables in the database:

npx prisma migrate dev --name init

The command does two things:

  • Saves the migration: Prisma Migrate takes a snapshot of your schema and figures out the SQL necessary to carry out the migration. The migration file containing the SQL is saved to prisma/migrations.
  • Runs the migration: Prisma Migrate executes the SQL in the migration file to create the database schema.

Checkpoint: You should see output similar to the following:

migrations/
  └─ 20260703110127_init/
    └─ migration.sql

Your database is now in sync with your schema.

Congratulations, you have successfully designed the data model and created the database schema. In the next step, you will use Prisma Client to query the database.

Generating Prisma Client

Prisma Client is a type-safe database client generated from your Prisma schema. It exposes a TypeScript API tailored to your models, with autocomplete and compile-time guarantees for every query.

Generate it with:

npx prisma generate

Checkpoint: You should see output similar to: ✔ Generated Prisma Client (7.8.0) to ./generated/prisma

The client is generated into generated/prisma in your project, as configured by the output option of the generator. Prisma ORM 7 generates the client into your project source rather than node_modules. Your dev server, bundler, and file watchers see the generated code as regular application code, and regenerating after a schema change no longer requires restarting tooling that caches node_modules.

Seeding the database

In this step, you will write a seed script that fills the database with sample data using Prisma Client CRUD operations. You will also use nested writes to create database rows for related entities in a single operation.

First, register the seed script in prisma.config.ts so the Prisma CLI knows how to run it:

import "dotenv/config";
import { defineConfig } from "prisma/config";

export default defineConfig({
  schema: "prisma/schema.prisma",
  migrations: {
    path: "prisma/migrations",
    seed: "tsx prisma/seed.ts",
  },
  datasource: {
    url: process.env["DATABASE_URL"],
  },
});

Then create prisma/seed.ts with the client setup. In Prisma ORM 7 you instantiate Prisma Client with a driver adapter, which manages the connection to PostgreSQL through node-postgres:

import "dotenv/config";
import { PrismaPg } from "@prisma/adapter-pg";
import { PrismaClient } from "../generated/prisma/client";

const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL! });
const prisma = new PrismaClient({ adapter });

function addDays(date: Date, days: number): Date {
  return new Date(date.getTime() + days * 24 * 60 * 60 * 1000);
}

async function main() {
  // Seed operations go here, step by step below
}

main()
  .then(async () => {
    await prisma.$disconnect();
  })
  .catch(async (e) => {
    console.error(e);
    await prisma.$disconnect();
    process.exit(1);
  });

Creating a user

Begin by creating a user inside the main function:

const grace = await prisma.user.create({
  data: {
    email: 'grace@hey.com',
    firstName: 'Grace',
    lastName: 'Bell',
    social: {
      bluesky: 'gracebell',
      linkedin: 'gracebell',
    },
  },
})

The operation creates a row in the User table and returns the created user, including the generated id. The returned grace value is fully typed: the User type is exported from your generated client, so you can also import it directly with import type { User } from "../generated/prisma/client".

Run the seed script with:

npx prisma db seed

As you follow the next steps, you will run the seed script more than once. To avoid hitting unique constraint errors, delete the contents of the database at the beginning of the main function:

await prisma.testResult.deleteMany({})
await prisma.courseEnrollment.deleteMany({})
await prisma.test.deleteMany({})
await prisma.user.deleteMany({})
await prisma.course.deleteMany({})

Note: These commands delete all rows in each database table. Use carefully and avoid this in production!

In this step, you will create a course and use a nested write to create related tests in the same operation. Add the following to the main function:

const weekFromNow = addDays(new Date(), 7)
const twoWeeksFromNow = addDays(new Date(), 14)
const monthFromNow = addDays(new Date(), 28)

const course = await prisma.course.create({
  data: {
    name: 'CRUD with Prisma',
    tests: {
      create: [
        { date: weekFromNow, name: 'First test' },
        { date: twoWeeksFromNow, name: 'Second test' },
        { date: monthFromNow, name: 'Final exam' },
      ],
    },
    members: {
      create: {
        role: 'TEACHER',
        user: {
          connect: {
            email: grace.email,
          },
        },
      },
    },
  },
  include: {
    tests: true,
  },
})

This creates one row in the Course table and three related rows in the Test table through their one-to-many relation. It also relates Grace to the course as a teacher through the explicit many-to-many relation: a row is created in the CourseEnrollment relation table with the TEACHER role.

When using nested writes, there are two options:

  • connect: Create a relation to an existing row
  • create: Create a new row and the relation to it

In the case of tests, you passed an array of objects to create which are all linked to the created course.

In the case of members, both create and connect were used: even though the user already exists, a new row in the CourseEnrollment relation table needs to be created, and connect links it to the existing user.

Note: The include argument fetches relations in the result. Here it returns the created tests, which you will need to relate test results to tests in a later step.

Creating users and relating them to a course

Next, create more users and relate them to the course as students:

const shakuntala = await prisma.user.create({
  data: {
    email: 'devi@prisma.io',
    firstName: 'Shakuntala',
    lastName: 'Devi',
    courses: {
      create: {
        role: 'STUDENT',
        course: {
          connect: { id: course.id },
        },
      },
    },
  },
})

const david = await prisma.user.create({
  data: {
    email: 'david@prisma.io',
    firstName: 'David',
    lastName: 'Deutsch',
    courses: {
      create: {
        role: 'STUDENT',
        course: {
          connect: { id: course.id },
        },
      },
    },
  },
})

Adding test results for the students

Looking at the TestResult model, it has three relations: student, gradedBy, and test. Adding a single test result looks as follows:

await prisma.testResult.create({
  data: {
    gradedBy: {
      connect: { email: grace.email },
    },
    student: {
      connect: { email: shakuntala.email },
    },
    test: {
      connect: { id: test.id },
    },
    result: 950,
  },
})

To add a test result for both David and Shakuntala for each of the three tests, loop over the course's tests:

const testResultsDavid = [650, 900, 950]
const testResultsShakuntala = [800, 950, 910]

let counter = 0
for (const test of course.tests) {
  await prisma.testResult.create({
    data: {
      gradedBy: { connect: { email: grace.email } },
      student: { connect: { email: shakuntala.email } },
      test: { connect: { id: test.id } },
      result: testResultsShakuntala[counter],
    },
  })

  await prisma.testResult.create({
    data: {
      gradedBy: { connect: { email: grace.email } },
      student: { connect: { email: david.email } },
      test: { connect: { id: test.id } },
      result: testResultsDavid[counter],
    },
  })

  counter++
}

Congratulations, you have created sample data for users, courses, tests, and test results in your database.

To explore the data visually, run Prisma Studio. Prisma Studio is a visual editor for your database:

npx prisma studio

Aggregating the test results with Prisma Client

Prisma Client can perform aggregate operations on the number fields (such as Int and Float) of a model. Aggregate operations compute a single result from a set of rows, for example the minimum, maximum, and average of the result column over a set of TestResult rows.

In this step, you will run two kinds of aggregate operations:

  1. For each test in the course across all students, resulting in aggregates representing how difficult the test was:

    for (const test of course.tests) {
      const results = await prisma.testResult.aggregate({
        where: {
          testId: test.id,
        },
        _avg: { result: true },
        _max: { result: true },
        _min: { result: true },
        _count: true,
      })
      console.log(`test: ${test.name} (id: ${test.id})`, results)
    }

    This results in the following:

    test: First test (id: 1) {
     _avg: { result: 725 },
     _max: { result: 800 },
     _min: { result: 650 },
     _count: 2
    }
    test: Second test (id: 2) {
     _avg: { result: 925 },
     _max: { result: 950 },
     _min: { result: 900 },
     _count: 2
    }
    test: Final exam (id: 3) {
     _avg: { result: 930 },
     _max: { result: 950 },
     _min: { result: 910 },
     _count: 2
    }
  2. For each student across all tests, resulting in aggregates representing the student's performance in the course:

    // Get aggregates for David
    const davidAggregates = await prisma.testResult.aggregate({
      where: {
        student: { email: david.email },
      },
      _avg: { result: true },
      _max: { result: true },
      _min: { result: true },
      _count: true,
    })
    console.log(`David's results (email: ${david.email})`, davidAggregates)
    
    // Get aggregates for Shakuntala
    const shakuntalaAggregates = await prisma.testResult.aggregate({
      where: {
        student: { email: shakuntala.email },
      },
      _avg: { result: true },
      _max: { result: true },
      _min: { result: true },
      _count: true,
    })
    console.log(`Shakuntala's results (email: ${shakuntala.email})`, shakuntalaAggregates)

    This results in the following terminal output:

    David's results (email: david@prisma.io) {
     _avg: { result: 833.3333333333334 },
     _max: { result: 950 },
     _min: { result: 650 },
     _count: 3
    }
    Shakuntala's results (email: devi@prisma.io) {
     _avg: { result: 886.6666666666666 },
     _max: { result: 950 },
     _min: { result: 800 },
     _count: 3
    }

Grouping aggregates with groupBy

Prisma Client also supports groupBy queries, which combine grouping and aggregation in a single database query. The per-test loop above can be expressed as one query:

const resultsByTest = await prisma.testResult.groupBy({
  by: ["testId"],
  _avg: { result: true },
  _max: { result: true },
  _min: { result: true },
  _count: true,
})

This returns one row per testId with the same aggregate values as the loop, using a single GROUP BY query in PostgreSQL instead of one query per test.

Summary and next steps

This article covered a lot of ground, starting with the problem domain and then delving into data modeling, the Prisma schema, database migrations with Prisma Migrate, CRUD operations with Prisma Client, and aggregations.

Mapping out the problem domain before jumping into code is generally good advice because it informs the design of the data model, which impacts every aspect of the backend. And because the Prisma schema is the single source of truth for both your database structure and your generated client, the data model you designed here carries through every layer of the application, whether the code is written by you, your team, or an AI agent working from the same schema.

While Prisma aims to make working with relational databases easy, it helps to have a deeper understanding of the underlying database. Check out Prisma's Data Guide to learn more about how databases work.

In the next parts of this series, you'll learn more about:

  • API layer
  • Validation
  • Testing
  • Authentication
  • Authorization
  • Integration with external APIs
  • Deployment

The database you created with npx create-db runs on Prisma Postgres, which you can claim and manage in the Prisma Console. If AI agents are part of your development workflow, the Prisma MCP server lets them create and manage databases as they need them. And when you reach the deployment part of this series, Prisma Compute runs your backend on the same platform as your database.

This series teaches Prisma ORM 7, the current production release. If you want to see where Prisma ORM is heading for AI-assisted development, read The Next Evolution of Prisma ORM. Prisma Next is the agent-native evolution of the ORM, available in Early Access today, and it becomes Prisma 8 at GA. It builds on the idea at the heart of this article: your schema acts as a single data contract, and every query against it is type-safe and returns structured output that humans and coding agents can build on safely. It runs in parallel with an existing project so you can migrate incrementally, and the performance benchmark covers what the rewrite means for throughput and client size.

Build your next app with Prisma

Start free. Scale when you’re ready.

Try Prisma
Share this article