kastor0.1.0
Get started

DocsFramework guides

kastor in your framework

15 short, complete setups, each with a full guide for project structure, connection reuse, environment variables and migrations on deploy. Each one creates a single client from the driver you choose and shows one read and one write the way that framework likes them. The schema is the one from the playground in src/db/schema.ts; the Postgres client module is in Get started.

FW 01App Router, server components, server actions

Next.js

Create the client once and reuse it across hot reloads in development, then query it from server components, server actions and route handlers. Never import the db module from a client component: the driver and your credentials must stay on the server.

src/db/index.ts
import pg from 'pg'
import { kastor } from 'kastor-orm'
import { postgres } from 'kastor-orm/postgres'
import { schema, relations } from './schema'

const create = () => kastor({
  adapter: postgres(new pg.Pool({ connectionString: process.env.DATABASE_URL })),
  schema,
  relations,
})

const g = globalThis as unknown as { db?: ReturnType<typeof create> }
export const db = g.db ?? create()
if (process.env.NODE_ENV !== 'production') g.db = db
app/posts/page.tsx
import { revalidatePath } from 'next/cache'
import { db } from '@/db'

export default async function Posts() {
  const posts = await db.posts.findMany({ include: { author: true }, take: 20 })

  async function publish(formData: FormData) {
    'use server'
    await db.posts.update({ where: { id: Number(formData.get('id')) }, data: { published: true } })
    revalidatePath('/posts')
  }

  return (
    <ul>
      {posts.map((p) => (
        <li key={p.id}>
          {p.title} by {p.author.email}
          <form action={publish}><input type="hidden" name="id" value={p.id} /><button>Publish</button></form>
        </li>
      ))}
    </ul>
  )
}

Read the full Next.js App Router guide

Next.js with PostgreSQL providers

FW 02Nitro server routes

Nuxt

Put the client in server/utils so Nitro auto imports it in every server route. Pages fetch through useFetch, so the database never reaches the browser bundle.

server/utils/db.ts
import pg from 'pg'
import { kastor } from 'kastor-orm'
import { postgres } from 'kastor-orm/postgres'
import { schema, relations } from '../db/schema'

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })

export const db = kastor({ adapter: postgres(pool), schema, relations })
server/api/posts.get.ts
import { db } from '../utils/db'

export default defineEventHandler(async (event) => {
  const { take } = getQuery(event)
  return db.posts.findMany({
    where: { published: true },
    include: { author: { select: { name: true } } },
    take: Number(take ?? 20),
  })
})

Read the full Nuxt guide

FW 03load functions and form actions

SvelteKit

Keep the client in $lib/server so SvelteKit refuses to ship it to the browser, then read in load functions and write in form actions.

src/lib/server/db.ts
import pg from 'pg'
import { env } from '$env/dynamic/private'
import { kastor } from 'kastor-orm'
import { postgres } from 'kastor-orm/postgres'
import { schema, relations } from './schema'

const pool = new pg.Pool({ connectionString: env.DATABASE_URL })

export const db = kastor({ adapter: postgres(pool), schema, relations })
src/routes/posts/+page.server.ts
import { db } from '$lib/server/db'

export const load = async () => ({
  posts: await db.posts.findMany({ orderBy: { id: 'desc' }, take: 20 }),
})

export const actions = {
  create: async ({ request }) => {
    const form = await request.formData()
    await db.posts.create({ data: { title: String(form.get('title')), authorId: 1 } })
  },
}

Read the full SvelteKit guide

FW 04loaders and actions in framework mode

Remix and React Router

Keep the client in a .server module so the bundler never ships it, read in loaders and write in actions. The same code works in React Router 7 framework mode and in Remix 2.

app/db.server.ts
import pg from 'pg'
import { kastor } from 'kastor-orm'
import { postgres } from 'kastor-orm/postgres'
import { schema, relations } from './db/schema'

const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL })

export const db = kastor({ adapter: postgres(pool), schema, relations })
app/routes/posts.tsx
import { redirect, useLoaderData, type ActionFunctionArgs } from 'react-router'
import { db } from '../db.server'

export async function loader() {
  return { posts: await db.posts.findMany({ where: { published: true }, include: { author: true }, take: 20 }) }
}

export async function action({ request }: ActionFunctionArgs) {
  const form = await request.formData()
  const post = await db.posts.create({ data: { title: String(form.get('title')), authorId: 1 } })
  return redirect('/posts/' + post.id)
}

export default function Posts() {
  const { posts } = useLoaderData<typeof loader>()
  return <ul>{posts.map((p) => <li key={p.id}>{p.title} by {p.author.email}</li>)}</ul>
}

Read the full Remix and React Router guide

FW 05on-demand pages and endpoints

Astro

With a server adapter, pages marked prerender = false and API endpoints run per request. Query kastor in the frontmatter or the endpoint; secrets without the PUBLIC_ prefix stay on the server.

src/pages/api/posts.ts
import type { APIRoute } from 'astro'
import { db } from '../../db'

export const prerender = false

export const GET: APIRoute = async ({ url }) => {
  const take = Number(url.searchParams.get('take') ?? 20)
  return Response.json(await db.posts.findMany({ where: { published: true }, include: { tags: true }, take }))
}

Read the full Astro guide

FW 06one client per process

Express

Create the client at startup and share it. Map KastorError codes to HTTP status codes in one error middleware.

src/server.ts
import express from 'express'
import { isKastorError } from 'kastor-orm'
import { db } from './db'

const app = express()
app.use(express.json())

app.get('/users/:id', async (req, res, next) => {
  try {
    const user = await db.users.findUnique({ where: { id: Number(req.params.id) }, include: { posts: true } })
    user ? res.json(user) : res.sendStatus(404)
  } catch (e) { next(e) }
})

app.use((err, req, res, next) => {
  if (isKastorError(err) && err.code === 'UNIQUE_VIOLATION') return res.status(409).json({ error: err.message })
  next(err)
})

app.listen(3000)

Read the full Express guide

FW 07decorate the instance

Fastify

Decorate the Fastify instance with the client and close the pool in the onClose hook so tests and graceful shutdowns finish cleanly.

src/app.ts
import Fastify from 'fastify'
import { db } from './db'

const app = Fastify({ logger: true })
app.decorate('db', db)
app.addHook('onClose', async () => { await db.$disconnect() })

app.get('/posts', async () => app.db.posts.findMany({ where: { published: true }, take: 50 }))

app.post<{ Body: { title: string; authorId: number } }>('/posts', async (req, reply) => {
  const post = await app.db.posts.create({ data: req.body })
  return reply.code(201).send(post)
})

await app.listen({ port: 3000 })

Read the full Fastify guide

FW 08a global module and a provider

NestJS

Register the client as a provider under a token in a global module, close the pool on shutdown, and inject it into services. Types come from the schema, so there are no entity classes or repositories to write.

src/db/db.module.ts
import { Global, Inject, Module, type OnApplicationShutdown } from '@nestjs/common'
import pg from 'pg'
import { kastor } from 'kastor-orm'
import { postgres } from 'kastor-orm/postgres'
import { schema, relations } from './schema'

export const KASTOR = Symbol('KASTOR')
export const createDb = (url: string) =>
  kastor({ adapter: postgres(new pg.Pool({ connectionString: url, max: 10 })), schema, relations })
export type Db = ReturnType<typeof createDb>

@Global()
@Module({
  providers: [{ provide: KASTOR, useFactory: () => createDb(process.env.DATABASE_URL!) }],
  exports: [KASTOR],
})
export class DbModule implements OnApplicationShutdown {
  constructor(@Inject(KASTOR) private readonly db: Db) {}
  async onApplicationShutdown() {
    await this.db.$disconnect()
  }
}
src/users/users.service.ts
import { Inject, Injectable } from '@nestjs/common'
import { KASTOR, type Db } from '../db/db.module'

@Injectable()
export class UsersService {
  constructor(@Inject(KASTOR) private readonly db: Db) {}

  find(id: number) {
    return this.db.users.findUniqueOrThrow({ where: { id }, include: { posts: true } })
  }
}

Read the full NestJS guide

FW 09Node, Bun, Deno and Workers

Hono

Hono runs everywhere, and so does kastor. On Node use the Postgres module above; on Cloudflare Workers build the client from the D1 binding inside the request.

src/index.ts
import { Hono } from 'hono'
import { kastor } from 'kastor-orm'
import { d1 } from 'kastor-orm/d1'
import { schema, relations } from './db/schema'

type Env = { Bindings: { DB: D1Database } }
const app = new Hono<Env>()

app.get('/users', async (c) => {
  const db = kastor({ adapter: d1(c.env.DB), schema, relations })
  return c.json(await db.users.findMany({ take: 20, include: { posts: true } }))
})

export default app

Read the full Hono guide

FW 10a service module next to or instead of Lucid

AdonisJS

Validate DATABASE_URL in start/env.ts, export the client from app/services/db.ts and import it through the #services alias in controllers. A provider closes the pool on shutdown.

app/services/db.ts
import pg from 'pg'
import { kastor } from 'kastor-orm'
import { postgres } from 'kastor-orm/postgres'
import env from '#start/env'
import { schema, relations } from '../../database/schema'

export default kastor({
  adapter: postgres(new pg.Pool({ connectionString: env.get('DATABASE_URL') })),
  schema,
  relations,
})
app/controllers/posts_controller.ts
import type { HttpContext } from '@adonisjs/core/http'
import db from '#services/db'

export default class PostsController {
  async index({ request }: HttpContext) {
    const take = Number(request.input('take', 20))
    return db.posts.findMany({ where: { published: true }, include: { author: true }, take })
  }
}

Read the full AdonisJS guide

FW 11bun:sqlite, no driver to install

Bun

Bun ships SQLite in the runtime. Pass a bun:sqlite Database to the sqlite adapter and you have a typed database with zero extra dependencies.

server.ts
import { Database } from 'bun:sqlite'
import { kastor } from 'kastor-orm'
import { sqlite } from 'kastor-orm/sqlite'
import { schema, relations } from './db/schema'

const db = kastor({ adapter: sqlite(new Database('app.db')), schema, relations })

Bun.serve({
  port: 3000,
  async fetch() {
    return Response.json(await db.posts.findMany({ include: { author: true }, take: 10 }))
  },
})

Read the full Bun guide

FW 12npm specifiers

Deno

Deno imports kastor and the driver with npm: specifiers. Grant network and env access when you run it.

main.ts
import pg from 'npm:pg'
import { kastor } from 'npm:kastor-orm'
import { postgres } from 'npm:kastor-orm/postgres'
import { schema, relations } from './db/schema.ts'

const db = kastor({
  adapter: postgres(new pg.Pool({ connectionString: Deno.env.get('DATABASE_URL') })),
  schema,
  relations,
})

Deno.serve(async () => Response.json(await db.users.count()))
Terminal
deno run --allow-net --allow-env main.ts

Read the full Deno guide

FW 13D1, or Postgres through a pooler

Cloudflare Workers

Bind a D1 database in wrangler.toml and build the client per request from env.DB. D1 runs batches, not interactive transactions, so use the array form of $transaction there.

wrangler.toml
name = "app"
main = "src/worker.ts"
compatibility_date = "2026-10-01"

[[d1_databases]]
binding = "DB"
database_name = "app"
database_id = "<your database id>"
src/worker.ts
import { kastor } from 'kastor-orm'
import { d1 } from 'kastor-orm/d1'
import { schema, relations } from './db/schema'

export default {
  async fetch(request: Request, env: { DB: D1Database }) {
    const db = kastor({ adapter: d1(env.DB), schema, relations })
    const url = new URL(request.url)
    if (request.method === 'POST') {
      const body = (await request.json()) as { email: string }
      return Response.json(await db.users.create({ data: { email: body.email } }), { status: 201 })
    }
    return Response.json(await db.users.findMany({ take: Number(url.searchParams.get('take') ?? 20) }))
  },
}

Read the full Cloudflare Workers guide

Cloudflare D1 in depth

FW 14SQLite in the main process, typed IPC

Electron

Open the SQLite file in the user data folder from the main process, apply the migrations shipped with the app, and expose a few IPC channels to the renderer through a preload bridge.

src/main/db.ts
import { join } from 'node:path'
import Database from 'better-sqlite3'
import { app, ipcMain } from 'electron'
import { kastor } from 'kastor-orm'
import { sqlite } from 'kastor-orm/sqlite'
import { deploy } from 'kastor-orm/migrate'
import { schema, relations } from './db/schema'

export async function startDatabase() {
  const adapter = sqlite(new Database(join(app.getPath('userData'), 'app.db')))
  await deploy(adapter, join(process.resourcesPath, 'migrations'))
  const db = kastor({ adapter, schema, relations })
  ipcMain.handle('posts:list', () => db.posts.findMany({ orderBy: { id: 'desc' }, take: 100 }))
  return db
}

Read the full Electron guide

FW 15reuse across warm invocations

AWS Lambda

Create the client at module scope so warm invocations reuse its connection, keep the pool at one connection per environment, and put RDS Proxy or another pooler in front of the database.

src/handler.ts
import pg from 'pg'
import { kastor } from 'kastor-orm'
import { postgres } from 'kastor-orm/postgres'
import { schema, relations } from './db/schema'

// Module scope: created at cold start, reused by warm invocations.
const db = kastor({ adapter: postgres(new pg.Pool({ connectionString: process.env.DATABASE_URL, max: 1 })), schema, relations })

export async function handler() {
  const posts = await db.posts.findMany({ where: { published: true }, take: 20 })
  return { statusCode: 200, body: JSON.stringify(posts) }
}

Read the full AWS Lambda guide

Suggestions

Kas looked under every log. Nothing matches that yet.

Try a method name, a database or a framework.