kastor0.1.0
Get started

GUIDE 33Deno

kastor with Deno

Deno runs npm packages through npm: specifiers, so kastor and its drivers work without a package.json. The client is plain TypeScript, Deno type checks it natively, and the same code runs locally, in a container or on Deno Deploy.

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.

deno.json
{
  "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"
  }
}
Project
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_URL

The 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.

db/client.ts
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.

main.ts
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
  }
})
Terminal
deno task dev
deno run --allow-net --allow-env --allow-read --env-file=.env main.ts

Permissions 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:

kastor.config.ts
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',
})
.github/workflows/deploy.yml
- 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.
  • nodeModulesDir is set so the CLI can run with Node.
  • CI runs deno check before npx 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 import npm:kastor-orm@^0.1.0/postgres directly.
  • Two env readers. Deno.env in the app and process.env in the config are the same environment, but --env-file only applies to deno run; the CLI reads .env next to the config on its own.
  • Type checking. deno check type checks your queries against the schema like tsc does; 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.

Suggestions

Kas looked under every log. Nothing matches that yet.

Try a method name, a database or a framework.