Project structure and imports
A Deno project describes its dependencies in deno.json. Map bare names to npm: specifiers once, and the code imports kastor-orm exactly as it would on Node. Setting nodeModulesDir to auto makes Deno write a node_modules folder, which the kastor CLI needs later.
{
"nodeModulesDir": "auto",
"imports": {
"kastor-orm": "npm:kastor-orm@^0.1.0",
"kastor-orm/postgres": "npm:kastor-orm@^0.1.0/postgres",
"pg": "npm:pg@^8"
},
"tasks": {
"dev": "deno run --watch --allow-net --allow-env --allow-read --env-file=.env main.ts",
"start": "deno run --allow-net --allow-env main.ts"
}
}deno.json imports and tasks
db/schema.ts tables and relations
db/client.ts the client
main.ts Deno.serve and routes
kastor.config.ts for the CLI (runs on Node)
kastor/migrations/
.env DATABASE_URLThe client
A Deno process loads each module once, so a client created at module scope is shared by every request. deno run --watch restarts the whole process on change, which closes the old pool with it.
import pg from 'pg'
import { kastor } from 'kastor-orm'
import { postgres } from 'kastor-orm/postgres'
import { schema, relations } from './schema.ts'
export const db = kastor({
adapter: postgres(new pg.Pool({ connectionString: Deno.env.get('DATABASE_URL'), max: 10 })),
schema,
relations,
})The schema file is ordinary TypeScript; note the explicit .ts extensions Deno expects in relative imports.
Serving routes with Deno.serve
Deno.serve takes a handler from Request to Response. URLPattern matches paths without a framework; for larger apps, Hono or Oak work the same way with kastor.
import { isKastorError } from 'kastor-orm'
import { db } from './db/client.ts'
const postById = new URLPattern({ pathname: '/posts/:id' })
Deno.serve({ port: Number(Deno.env.get('PORT') ?? 8000) }, async (req: Request) => {
const url = new URL(req.url)
try {
if (url.pathname === '/posts' && req.method === 'GET') {
return Response.json(await db.posts.findMany({ where: { published: true }, include: { author: true }, take: 20 }))
}
if (url.pathname === '/posts' && req.method === 'POST') {
const body = (await req.json()) as { title: string; authorId: number }
return Response.json(await db.posts.create({ data: body }), { status: 201 })
}
const match = postById.exec(url)
if (match) {
const post = await db.posts.findUnique({ where: { id: Number(match.pathname.groups.id) }, include: { tags: true } })
return post ? Response.json(post) : new Response('Not found', { status: 404 })
}
return new Response('Not found', { status: 404 })
} catch (e) {
if (isKastorError(e)) return Response.json({ error: e.code, message: e.message }, { status: 400 })
throw e
}
})deno task dev
deno run --allow-net --allow-env --allow-read --env-file=.env main.tsPermissions are part of the security model: --allow-net can be narrowed to the database host and the listen port, for example --allow-net=db.internal:5432,0.0.0.0:8000.
Migrations with the CLI
The kastor CLI runs on Node.js and loads kastor.config.ts with its own TypeScript loader. After deno install (or any deno run) has populated node_modules, Node can resolve kastor and pg from it:
import pg from 'pg'
import { defineConfig } from 'kastor-orm'
import { postgres } from 'kastor-orm/postgres'
export default defineConfig({
schema: './db/schema.ts',
adapter: () => postgres(new pg.Pool({ connectionString: process.env.DATABASE_URL })),
migrations: './kastor/migrations',
})- uses: denoland/setup-deno@v2
- uses: actions/setup-node@v4
with: { node-version: 22 }
- run: deno install
- run: deno check main.ts
- run: npx kastor migrate deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}The config reads process.env because it runs under Node; the app reads Deno.env. Both see the same variable.
Deploying
In a container, use the official Deno image and deno task start, with migrations applied from CI before the new container starts. On Deno Deploy, set DATABASE_URL in the project settings; outbound TCP to Postgres works there, and a pooled connection string keeps connection counts low across isolates. Each isolate holds its own pool, so keep max small.
For SQLite, recent Deno versions provide node:sqlite; pass a DatabaseSync to the sqlite adapter and grant --allow-read and --allow-write on the file. On Deno Deploy, where there is no persistent disk, use Turso with npm:@libsql/client and the libsql adapter instead.
Testing with Deno.test
Deno's built in test runner works well with kastor because the client is just a value. Point DATABASE_URL at a test database, apply migrations once before the run with npx kastor migrate deploy, and empty the tables you touch at the start of each test with db.$execute. Close the pool at the end of the file so the runner's resource sanitizer does not report a leak: Deno checks that tests do not leave sockets open, and an open pool counts.
For fast unit tests of query logic, use SQLite in memory through node:sqlite with the sqlite adapter and create the tables with npx kastor db push against a scratch file, or by running the migration SQL at startup. The same schema compiles to SQLite and PostgreSQL, so most query logic can be tested without a server.
Production checklist
Before the first deploy, check these points. Each one prevents a problem that otherwise shows up only under real traffic, usually as exhausted connections, a leaked secret or a schema that does not match the code that is running:
- deno.json maps
kastor-orm, each adapter subpath and the driver. - Permissions are as narrow as the app needs, ideally with hosts on
--allow-net. nodeModulesDiris set so the CLI can run with Node.- CI runs
deno checkbeforenpx kastor migrate deploy. - Deno Deploy projects use a pooled URL and a small pool per isolate.
Pitfalls
- Missing permissions. A failed connection with a permission error is Deno's sandbox, not the driver. Add the flag the message names.
- Subpath imports. Map
kastor-orm/postgres(and any other adapter) in deno.json, or importnpm:kastor-orm@^0.1.0/postgresdirectly. - Two env readers.
Deno.envin the app andprocess.envin the config are the same environment, but--env-fileonly applies todeno run; the CLI reads.envnext to the config on its own. - Type checking.
deno checktype checks your queries against the schema liketscdoes; run it in CI.
Questions people ask
Q1Does kastor work in Deno?
Yes. Import it with npm:kastor-orm or map it in deno.json. The core has no Node only imports, and drivers such as pg and mysql2 run through Deno's Node compatibility.
Q2Which permissions does a kastor app need on Deno?
--allow-net to reach the database, --allow-env to read DATABASE_URL, and --allow-read for SQLite files or an env file.
Q3How do I run kastor migrations in a Deno project?
The CLI is a Node.js program. With nodeModulesDir: 'auto', Deno writes a node_modules folder, and npx kastor migrate deploy runs against it in CI or locally.
Q4Can I use SQLite with kastor on Deno?
Recent Deno versions implement node:sqlite, which the sqlite adapter accepts. For a hosted option, Turso through npm:@libsql/client works too.