Skip to content
Discord Get Started

Express / Hono

Express and Hono connect to DB9 through any standard PostgreSQL driver over pgwire. DB9 is PostgreSQL-compatible, so no special adapter is needed.

Terminal
db9 create --name express-app

db9 create prints the connection string, with the admin password, once — copy it from the Connection String: line. It has this shape:

Output
postgresql://YOUR_TENANT_ID.admin:YOUR_PASSWORD@pg.d0000.db9.io:5432/postgres

YOUR_TENANT_ID is the tenant ID prefix in the generated connection string. Copy the username from the Connection String: line printed by db9 create or from db9 db connect. The examples below use pg.db9.io:5433, which reaches the same server; either host and port works. db9 db status express-app shows the connection string without the password; if you lose it, db9 db reset-password express-app sets a new password and prints a new connection string.

Set the connection string as an environment variable:

.env
DATABASE_URL="postgresql://YOUR_TENANT_ID.admin:YOUR_PASSWORD@pg.db9.io:5433/postgres?sslmode=require"
Terminal
mkdir express-db9 && cd express-db9
npm init -y
npm install express @prisma/client@6 dotenv
npm install -D prisma@6 typescript @types/express @types/node tsx
npx prisma init
npx tsc --init

Set DATABASE_URL in .env (see above), then define your schema:

prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String
createdAt DateTime @default(now()) @map("created_at")
@@map("users")
}

Push the schema and generate the client:

Terminal
npx prisma db push
npx prisma generate

This first push works against an empty database. Later schema changes need raw SQL — see Troubleshooting.

src/index.ts
import express from 'express';
import { PrismaClient } from '@prisma/client';
import 'dotenv/config';
const app = express();
const prisma = new PrismaClient();
app.use(express.json());
app.get('/users', async (_req, res) => {
const users = await prisma.user.findMany({
orderBy: { createdAt: 'desc' },
});
res.json(users);
});
app.post('/users', async (req, res) => {
const user = await prisma.user.create({
data: { email: req.body.email, name: req.body.name },
});
res.status(201).json(user);
});
app.get('/users/:id', async (req, res) => {
const user = await prisma.user.findUnique({
where: { id: Number(req.params.id) },
});
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
});
app.delete('/users/:id', async (req, res) => {
await prisma.user.delete({ where: { id: Number(req.params.id) } });
res.status(204).end();
});
app.listen(3000, () => console.log('Listening on http://localhost:3000'));
src/index.ts
import { Prisma } from '@prisma/client';
import { Request, Response, NextFunction } from 'express';
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
if (err instanceof Prisma.PrismaClientKnownRequestError) {
if (err.code === 'P2002') {
return res.status(409).json({ error: 'Duplicate entry' });
}
}
console.error(err);
res.status(500).json({ error: 'Internal server error' });
});
  • Host and port: pg.db9.io:5433 and the pg.d0000.db9.io:5432 that db9 create prints reach the same server; port 5432 works on either host.
  • TLS: Always include sslmode=require in the connection string for DB9’s hosted service.
  • Connection pooling: Start with 5-10 connections (max option in pg.Pool, or Prisma’s connection_limit URL parameter). DB9 handles per-tenant pooling server-side.
  • Graceful shutdown: Close the database pool when the process exits to avoid leaked connections:
src/index.ts
process.on('SIGTERM', async () => {
await prisma.$disconnect();
process.exit(0);
});

DB9 accepts port 5432 as well as 5433, so the client is not reaching DB9 at all. A refusal from localhost or 127.0.0.1 means the connection string never reached the driver — check that DATABASE_URL is set in the process that opens the connection.

If you see “too many connections” errors, reduce the pool size and confirm you are not creating a new pool on every request. Reuse a single pool instance across the application.

DB9 supports TLS and libpq negotiates it by default, but the server does not require it — make sure your connection string includes ?sslmode=require so a stray disable cannot downgrade you. If you configure pg.Pool options directly, set ssl: true or ssl: { rejectUnauthorized: false } for development.

prisma db push works against an empty database and then stops applying changes once tables exist: Prisma does not recognise DB9’s spelling of a serial column’s default, so every later push tries to create each autoincrement() sequence again. With lower-case table names it fails with relation "…_id_seq" already exists; with Prisma’s default upper-case names ("User") it reports success but re-points id at a new sequence, so inserts into a table with rows then fail with 23505 — see the Prisma guide. drizzle-kit push works only for the first push into a schema with no tables: after that its introspection reads pg_stat_user_indexes for each existing table, which DB9 does not implement, so every later push applies nothing. Create the initial schema with prisma db push or with raw DDL and apply later changes with raw DDL — or, for Drizzle, manage the schema from the start with drizzle-kit generate and drizzle-kit migrate. See the Prisma guide and the Drizzle guide.