PostgreSQL Integration
Level: Beginner · Time: ~20 minutes · Prerequisites: Your First API, a running PostgreSQL
StreetJS ships a native PostgreSQL wire driver (SCRAM-SHA-256 auth, pooling)
— no pg dependency. This tutorial wires a pool, runs migrations, and reads/writes
data with the repository pattern.
1. Start PostgreSQL
The scaffold includes a docker-compose.yml with Postgres:
1
docker compose up -d postgres
Set credentials in .env (copy from .env.example):
1
2
3
4
5
PG_HOST=localhost
PG_PORT=5432
PG_DATABASE=street
PG_USER=street
PG_PASSWORD=street_pass
2. Create the pool
PgPool manages connections. Register it in the container so repositories can
resolve it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// src/main.ts
import { PgPool, container } from 'streetjs';
const pool = new PgPool({
host: process.env['PG_HOST'] ?? 'localhost',
port: parseInt(process.env['PG_PORT'] ?? '5432', 10),
user: process.env['PG_USER'] ?? 'postgres',
password: process.env['PG_PASSWORD'] ?? '',
database: process.env['PG_DATABASE'] ?? 'street',
minConnections: 2,
maxConnections: 10,
idleTimeoutMs: 30_000,
acquireTimeoutMs: 5_000,
});
await pool.initialize(); // warm up min connections
container.register(PgPool, pool);
3. Write a migration
Migrations are plain SQL files in migrations/, applied in filename order. Create
migrations/20260101000000_create_tasks.sql:
1
2
3
4
5
6
7
CREATE TABLE tasks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(200) NOT NULL,
done BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX tasks_created_at_idx ON tasks (created_at DESC);
Run migrations on startup:
1
2
3
4
import { StreetMigrationRunner } from 'streetjs';
const runner = new StreetMigrationRunner(pool);
await runner.run('./migrations');
…or from the CLI:
1
2
street migrate:run
street migrate:create add_priority_to_tasks # scaffolds a timestamped file
Note —
gen_random_uuid()requires thepgcryptoextension. The scaffold enables it viadocker-init/001_enable_pgcrypto.sql. On a managed database runCREATE EXTENSION IF NOT EXISTS pgcrypto;once.
4. The repository pattern
Repositories own data access. Always use parameterized queries ($1, $2)
— never string interpolation — to stay injection-safe.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// src/repositories/task.repository.ts
import { Injectable, container, PgPool } from 'streetjs';
import type { PgRow } from 'streetjs';
export interface Task { id: string; title: string; done: boolean; createdAt: Date; }
function rowToTask(row: PgRow): Task {
return {
id: String(row['id']),
title: String(row['title']),
done: Boolean(row['done']),
createdAt: new Date(String(row['created_at'])),
};
}
@Injectable()
export class TaskRepository {
private readonly pool = container.resolve(PgPool);
async list(): Promise<Task[]> {
const res = await this.pool.query('SELECT * FROM tasks ORDER BY created_at DESC');
return res.rows.map(rowToTask);
}
async create(title: string): Promise<Task> {
const res = await this.pool.query(
'INSERT INTO tasks (title) VALUES ($1) RETURNING *',
[title],
);
return rowToTask(res.rows[0] as PgRow);
}
async setDone(id: string, done: boolean): Promise<void> {
await this.pool.query('UPDATE tasks SET done = $1 WHERE id = $2', [done, id]);
}
async remove(id: string): Promise<void> {
await this.pool.query('DELETE FROM tasks WHERE id = $1', [id]);
}
}
Tip — integer columns. The native driver returns some numeric/
COUNT(*)columns as strings. Coerce explicitly withNumber(...)/parseInt(...)when you need a number (e.g.parseInt(String(row['total']), 10)).
5. Use it from a service + controller
1
2
3
4
5
6
7
8
9
import { Injectable } from 'streetjs';
import { TaskRepository } from '../repositories/task.repository.js';
@Injectable()
export class TaskService {
constructor(private readonly repo: TaskRepository) {}
list() { return this.repo.list(); }
add(title: string) { return this.repo.create(title); }
}
The controller follows the same pattern as the REST API example, which shows full CRUD, pagination, and OpenAPI generation end to end.
Going further: the ORM
For relations and model-driven migrations, add
@streetjs/orm: entity decorators
(@Entity, @Column, @HasMany, …), eager loading (N+1-safe), and
Orm.makeMigration to diff your models against the live schema.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
password authentication failed |
Check .env credentials match the running Postgres. |
function gen_random_uuid() does not exist |
Run CREATE EXTENSION IF NOT EXISTS pgcrypto;. |
A count comes back as "42" (string) |
Expected — coerce with parseInt(String(value), 10). |
| Pool acquire timeouts under load | Raise maxConnections or lower acquireTimeoutMs and add backpressure. |