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.
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 = dbimport { 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>
)
}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.
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 })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),
})
})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.
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 })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 } })
},
}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.
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 })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>
}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.
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 }))
}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.
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)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.
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 })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.
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()
}
}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 } })
}
}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.
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 appFW 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.
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,
})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 })
}
}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.
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 }))
},
})FW 12npm specifiers
Deno
Deno imports kastor and the driver with npm: specifiers. Grant network and env access when you run it.
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()))deno run --allow-net --allow-env main.tsFW 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.
name = "app"
main = "src/worker.ts"
compatibility_date = "2026-10-01"
[[d1_databases]]
binding = "DB"
database_name = "app"
database_id = "<your database id>"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) }))
},
}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.
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
}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.
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) }
}