Zero dependencies: what we deleted to fit on the edge

UQL is a dependency-free TypeScript ORM for PostgreSQL, MySQL, MariaDB, SQLite and MongoDB.
npm i uql-orm installs one package. 288 kB on the wire, no dependencies, every dialect included: PostgreSQL, CockroachDB, MySQL, MariaDB, SQLite, Turso, libSQL, Neon, Cloudflare D1, Bun SQL and MongoDB.
A month ago that wasn’t true. In July it pulled in four mandatory packages and unpacked to about 4 MB. Most of what follows is deletion.
What it looks like
Section titled “What it looks like”The Turso Cloud client talks HTTP through fetch(), and UQL has no runtime dependencies, so this is a whole Cloudflare Worker:
import { TursoQuerierPool } from 'uql-orm/turso';import { Todo } from './entities';
const pool = new TursoQuerierPool({ url: env.TURSO_DATABASE_URL, authToken: env.TURSO_AUTH_TOKEN,});
export default { async fetch() { const todos = await pool.findMany(Todo, { $limit: 50 }); return Response.json(todos); },};No build plugin, no nodejs_compat, no bundler aliases. The driver is resolved on first use, so the pool can sit at module scope without loading anything until a request arrives.
What one npm i puts on disk
Section titled “What one npm i puts on disk”I’m the author, so don’t take my word for it. Install one package into an empty project and count the bytes yourself:
npm i --omit=dev uql-ormfind node_modules -type f -exec cat {} + | wc -cThat’s unpacked bytes on disk, not the 288 kB you download. Tarballs compress well; cold starts don’t.
| Package | Installed | Files |
|---|---|---|
uql-orm 0.24.1 |
1.0 MB | 384 |
kysely 0.29.4 |
1.6 MB | 611 |
@mikro-orm/postgresql 7.1.9 |
4.7 MB | 1,153 |
drizzle-orm 0.45.2 |
9.9 MB | 2,667 |
sequelize 6.37.8 |
15.0 MB | 2,708 |
typeorm 1.1.0 |
22.5 MB | 3,663 |
@prisma/client 7.9.1 |
75.0 MB | 94 |
Two caveats, both cutting against the headline. kysely is a query builder rather than an ORM, so it’s doing less by design. And 93% of @prisma/client is one thing: 70 MB of Rust query compilers cross-compiled to WebAssembly and base64-encoded into JavaScript, one per engine it supports. UQL ships every dialect for the same reason, so that isn’t fair to hold against Prisma on its own. The comparison that does hold is per dialect: its PostgreSQL compiler alone is 4.9 MB in the fast build, against 19.9 kB gzipped for UQL’s entire PostgreSQL entry point.
No row includes a driver either. Add pg to any of them and they all grow by the same amount. What the table shows is the floor.
What had to go
Section titled “What had to go”Four mandatory dependencies, each carried for one job.
reflect-metadata (264 KB) was there for a single call, Reflect.getMetadata('design:type', ...), so that @Field() could infer a column type from the TypeScript type. Standard decorators made it unnecessary, and that’s its own post.
jiti (1.8 MB) is a complete TypeScript transpiler, shipped so uql-migrate could read a uql.config.ts. Nearly two megabytes of every install existed to load one config file. It’s gone, and uql.config.ts now needs a runtime that already transforms TypeScript (bun, or node --import tsx). Bundling a transpiler was never the right call anyway: only your runtime knows your tsconfig.json.
tslib left with "importHelpers": false. The inlined helpers cost a few hundred bytes; the import cost a whole dependency edge.
sqlstring handled MySQL and MariaDB inline literals. Dialect.escape now shares the Postgres and SQLite value handling, byte-for-byte identical across 29 value shapes except where sqlstring was simply wrong: it rendered a Uint8Array as `0` = 255 instead of X'ff00', and a plain object as '[object Object]'.
Then the things that were never code at all: 369 sourcemaps and a 108 KB changelog that shipped in every install, gone by narrowing files to ["dist", "README.md"] and turning off declarationMap.
Nothing loads that you didn’t ask for
Section titled “Nothing loads that you didn’t ask for”Install size is the number people quote, but cold start depends on how much actually gets imported. UQL is ESM-only, sets "sideEffects": false, and exposes 28 export subpaths, so a Postgres app never touches the MongoDB or MySQL module graphs. Minified and gzipped, with peers external:
| Entry point | Gzipped |
|---|---|
uql-orm |
24.5 kB |
uql-orm/postgres |
19.9 kB |
uql-orm/turso |
18.9 kB |
uql-orm/migrate |
31.6 kB |
uql-orm/browser |
1.6 kB |
That last row is the HTTP client: fetch and a query serializer, which is all the browser needs when the server is the thing holding a connection.
Sizes regress the way performance regresses, quietly, one convenience import at a time, so those numbers are checked against hardcoded budgets on every build and again on prepack. The root entry currently measures 24,505 bytes gzipped against a 24,600 limit, so there are 95 bytes of headroom. The next careless import fails the build instead of the next release.
The same script walks the module graph of dist/index.js, dist/browser/index.js and dist/http/index.js and refuses to pack if any of them reaches a Node builtin. That check exists because 0.13.0 shipped and had to be pulled: its root import dragged node:async_hooks into browser bundles. AsyncLocalStorage now sits behind a browser field remap, with a shim that throws a server-only message instead.
What the small number doesn’t cover
Section titled “What the small number doesn’t cover”The small number is UQL, not your database driver. pg, mysql2, mariadb, mongodb and better-sqlite3 have their own weight, and better-sqlite3 needs a native build. What UQL guarantees is that it adds nothing on top of whichever one you choose, and that the ones which are pure fetch (Turso Cloud, Neon, D1) stay that way end to end. @tursodatabase/database ships native binaries and isn’t edge-safe, which is why it lives behind its own entry point rather than being reachable from the Cloud path.
Get started
Section titled “Get started”npm i uql-orm- Quick Start
- Turso & libSQL and Cloudflare D1 for the edge-safe drivers, Serverless for pooling in functions that freeze
- Browser client
- How UQL compares
- GitHub
If your install contains something that shouldn’t be there, open an issue. Every number above is one command away from being checked.