Skip to content
NewFrom Mongoose to SQL6 min read

Quick Start

UQL is a TypeScript ORM whose queries are plain, typed JSON objects. Entities are plain classes, with no code generation. This page takes you from install to a first query.

Terminal window
npm install uql-orm pg # or mysql2, better-sqlite3, mongodb, etc.
npm install -D tsx # the CLI runs uql.config.ts through it on Node

UQL runs on Node 22.18+, Bun and edge runtimes such as Cloudflare Workers, with TypeScript 5.7 or newer. It is ESM only; a CommonJS app loads it through require(esm). Three tsconfig.json settings matter:

tsconfig.json
{
"compilerOptions": {
"module": "nodenext", // or "preserve": the resolver must read `exports`
"target": "es2024", // any dated target from es2022; `esnext` leaves decorators untransformed
"lib": ["esnext"] // `await using` needs `AsyncDisposable`
}
}

Behind a bundler, "module": "preserve" (TypeScript 5.4+) or "moduleResolution": "bundler" also works. Vite’s template pins lib to ES2023, so add esnext. Next.js needs a Babel config for the decorators. If Node runs your compiled output, set "type": "module" in package.json.

entities.ts
import { Entity, Id, Field } from 'uql-orm';
@Entity()
export class User {
@Id({ type: 'uuid', onInsert: () => crypto.randomUUID() })
id!: string;
@Field({ type: String, unique: true, nullable: false })
email!: string;
@Field({ type: String })
name?: string | null;
}
uql.config.ts
import type { Config } from 'uql-orm';
import { PgQuerierPool } from 'uql-orm/postgres';
import { User } from './entities.js';
export const pool = new PgQuerierPool({
host: 'localhost',
user: 'postgres',
password: 'password',
database: 'uql_app',
});
export default { pool, entities: [User] } satisfies Config;

Create the pool once per process and import it everywhere. It connects on the first query. Other databases and pool sizing are in Pool.

Terminal window
npx uql-migrate sync

sync compares the entities in uql.config.ts with the database and creates what is missing. Add --dry-run to print the DDL instead. Once the data matters, switch to generate:entities, which writes the diff to a migration you review. For tables that already exist, generate:from-db writes the entities.

app.ts
import { pool } from './uql.config.js';
import { User } from './entities.js';
await pool.insertMany(User, [
{ email: 'ada@uql-orm.dev', name: 'Ada' },
{ email: 'grace@example.com', name: 'Grace' },
]);
const users = await pool.findMany(User, {
$select: { id: true, name: true },
$where: { email: { $endsWith: '@uql-orm.dev' } },
});
// [{ id: '...', name: 'Ada' }]

Each pool call takes a connection and releases it. To share one connection across calls, use pool.withQuerier, or pool.transaction to commit them together. See pool vs. querier.