Elysia

Learn how to use Prisma ORM in an Elysia app

Introduction

Elysia is an ergonomic web framework for building high-performance backend servers with Bun. It offers end-to-end type safety, an expressive API, and exceptional performance. Combined with Prisma ORM and Prisma Postgres, you get a fast, type-safe backend stack.

In this guide, you'll learn to integrate Prisma ORM with a Prisma Postgres database in an Elysia application. You can find a complete example of this guide on GitHub.

Prerequisites

  • Bun installed on your system

1. Set up your project

Create a new Elysia project using the Bun scaffolding command:

bun create elysia elysia-prisma

Navigate to the project directory:

cd elysia-prisma

2. Install and configure Prisma

2.1. Install dependencies

Install the required Prisma packages, database adapter, and Prismabox (for generated TypeBox schemas):

bun add -d prisma bun-types
bun add @prisma/client @prisma/adapter-pg pg prismabox

If you are using a different database provider (MySQL, SQL Server, SQLite), install the corresponding driver adapter package instead of @prisma/adapter-pg. For more information, see Database drivers.

Once installed, initialize Prisma in your project:

bunx --bun prisma init --db --output ../src/generated/prisma

You'll need to answer a few questions while setting up your Prisma Postgres database. Select the region closest to your location and a memorable name for your database like "My Elysia Project"

This will create:

  • A prisma/ directory with a schema.prisma file
  • A prisma.config.ts file for configuring Prisma
  • A .env file with a DATABASE_URL already set

2.2. Define your Prisma Schema

In the prisma/schema.prisma file, mirror the example app structure with Prismabox TypeBox generators and a Todo model:

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

generator prismabox {
  provider                    = "prismabox"
  typeboxImportDependencyName = "elysia"
  typeboxImportVariableName   = "t"
  inputModel                  = true
  output                      = "../src/generated/prismabox"
}

datasource db {
  provider = "postgresql"
}

model Todo { 
  id        Int      @id @default(autoincrement()) 
  title     String
  completed Boolean  @default(false) 
  createdAt DateTime @default(now()) 
  updatedAt DateTime @updatedAt
} 

This matches the Prisma Elysia example: it generates Prisma Client to src/generated/prisma and Prismabox TypeBox schemas to src/generated/prismabox.

What is Prismabox?

  • A Prisma generator that reads your Prisma schema and emits Elysia-friendly TypeBox models.
  • Generates files like src/generated/prismabox/Todo.ts (and one per model) with TodoPlain, TodoPlainInputCreate, etc.
  • Use those generated models in routes to validate requests/responses and keep Elysia types in sync with your Prisma schema (also useful for OpenAPI/Eden).

2.3. Run migrations and generate Prisma Client

Run the following commands to create the database tables and generate the Prisma Client:

bunx --bun prisma migrate dev --name init
bunx --bun prisma generate

2.4. Seed the database

Add some seed data to populate the database with sample todos (mirrors the example repo).

Create a new file called seed.ts in the prisma/ directory:

prisma/seed.ts
import { PrismaClient } from "../src/generated/prisma/client.js";
import { PrismaPg } from "@prisma/adapter-pg";

if (!process.env.DATABASE_URL) {
  throw new Error("DATABASE_URL is not set");
}

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

const todoData = [
  { title: "Learn Elysia" },
  { title: "Learn Prisma" },
  { title: "Build something awesome", completed: true },
];

async function main() {
  console.log("Start seeding...");
  for (const todo of todoData) {
    const created = await prisma.todo.create({
      data: todo,
    });
    console.log(`Created todo with id: ${created.id}`);
  }
  console.log("Seeding finished.");
}

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

Run the seed script:

bunx --bun prisma db seed

And open Prisma Studio to inspect your data:

bunx --bun prisma studio

3. Integrate Prisma into Elysia

3.1. Create a Prisma Client instance

Inside the src/ directory, create a lib directory with a prisma.ts file. This file will create and export your Prisma Client instance and add the following code:

src/lib/prisma.ts
import { PrismaClient } from "../generated/prisma/client.js";
import { PrismaPg } from "@prisma/adapter-pg";

const databaseUrl = process.env.DATABASE_URL;

if (!databaseUrl) {
  throw new Error("DATABASE_URL is not set");
}

const adapter = new PrismaPg({ connectionString: databaseUrl });
export const prisma = new PrismaClient({ adapter });

3.2. Create API routes

Update your src/index.ts file to match the Prisma Elysia example, including Prismabox-generated validation types:

src/index.ts
import { Elysia, t } from "elysia";
import { prisma } from "./lib/prisma";
import { TodoPlain, TodoPlainInputCreate, TodoPlainInputUpdate } from "./generated/prismabox/Todo";

const app = new Elysia()
  // Health check
  .get("/", () => {
    return { message: "Hello Elysia with Prisma!" };
  })

  // Get all todos
  .get(
    "/todos",
    async () => {
      const todos = await prisma.todo.findMany({
        orderBy: { createdAt: "desc" },
      });
      return todos;
    },
    {
      response: t.Array(TodoPlain),
    },
  )

  // Get a single todo by ID
  .get(
    "/todos/:id",
    async ({ params, set }) => {
      const id = Number(params.id);
      const todo = await prisma.todo.findUnique({
        where: { id },
      });

      if (!todo) {
        set.status = 404;
        return { error: "Todo not found" };
      }

      return todo;
    },
    {
      params: t.Object({
        id: t.Numeric(),
      }),
      response: {
        200: TodoPlain,
        404: t.Object({
          error: t.String(),
        }),
      },
    },
  )

  // Create a new todo
  .post(
    "/todos",
    async ({ body }) => {
      const todo = await prisma.todo.create({
        data: {
          title: body.title,
        },
      });
      return todo;
    },
    {
      body: TodoPlainInputCreate,
      response: TodoPlain,
    },
  )

  // Update a todo
  .put(
    "/todos/:id",
    async ({ params, body, set }) => {
      const id = Number(params.id);

      try {
        const todo = await prisma.todo.update({
          where: { id },
          data: {
            title: body.title,
            completed: body.completed,
          },
        });
        return todo;
      } catch {
        set.status = 404;
        return { error: "Todo not found" };
      }
    },
    {
      params: t.Object({
        id: t.Numeric(),
      }),
      body: TodoPlainInputUpdate,
      response: {
        200: TodoPlain,
        404: t.Object({
          error: t.String(),
        }),
      },
    },
  )

  // Toggle todo completion
  .patch(
    "/todos/:id/toggle",
    async ({ params, set }) => {
      const id = Number(params.id);

      try {
        const todo = await prisma.todo.findUnique({
          where: { id },
        });

        if (!todo) {
          set.status = 404;
          return { error: "Todo not found" };
        }

        const updated = await prisma.todo.update({
          where: { id },
          data: { completed: !todo.completed },
        });

        return updated;
      } catch {
        set.status = 404;
        return { error: "Todo not found" };
      }
    },
    {
      params: t.Object({
        id: t.Numeric(),
      }),
      response: {
        200: TodoPlain,
        404: t.Object({
          error: t.String(),
        }),
      },
    },
  )

  // Delete a todo
  .delete(
    "/todos/:id",
    async ({ params, set }) => {
      const id = Number(params.id);

      try {
        const todo = await prisma.todo.delete({
          where: { id },
        });
        return todo;
      } catch {
        set.status = 404;
        return { error: "Todo not found" };
      }
    },
    {
      params: t.Object({
        id: t.Numeric(),
      }),
      response: {
        200: TodoPlain,
        404: t.Object({
          error: t.String(),
        }),
      },
    },
  )

  .listen(3000);

console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`);

This creates the same endpoints as the official example:

  • GET / - Health check
  • GET /todos - Fetch all todos (newest first)
  • GET /todos/:id - Fetch a single todo
  • POST /todos - Create a todo
  • PUT /todos/:id - Update a todo
  • PATCH /todos/:id/toggle - Toggle completion
  • DELETE /todos/:id - Delete a todo

Prismabox generates the TodoPlain/TodoPlainInput* TypeBox schemas so responses and request bodies are validated and typed.

3.3. Run the application

Start your Elysia server:

bun run dev

You should see 🦊 Elysia is running at localhost:3000 in the console.

3.4. Test the API

Test the endpoints using curl:

# Health check
curl http://localhost:3000/ | jq

# Get all todos
curl http://localhost:3000/todos | jq

# Get a single todo
curl http://localhost:3000/todos/1 | jq

# Create a new todo
curl -X POST http://localhost:3000/todos \
  -H "Content-Type: application/json" \
  -d '{"title": "Ship the Prisma + Elysia guide"}' | jq

# Toggle completion
curl -X PATCH http://localhost:3000/todos/1/toggle | jq

# Update a todo
curl -X PUT http://localhost:3000/todos/1 \
  -H "Content-Type: application/json" \
  -d '{"title": "Updated title", "completed": true}' | jq

# Delete a todo
curl -X DELETE http://localhost:3000/todos/1 | jq

You're done! You've created an Elysia app with Prisma that's connected to a Prisma Postgres database.

Next steps

Now that you have a working Elysia app connected to a Prisma Postgres database, you can:

  • Extend your Prisma schema with more models and relationships
  • Add update and delete endpoints
  • Explore authentication with Elysia plugins
  • Enable query caching with Prisma Postgres for better performance
  • Use Eden for end-to-end type-safe API calls

More info

On this page