Add a Database
Define an entity with decorators, generate migrations, and persist your notes in Cloudflare D1.
By the end of this tutorial your notes will survive a restart: you'll define a database entity with decorators, generate and apply a SQL migration, and rewrite NoteService to read and write Cloudflare D1 through Base's ORM.
You'll continue with the notes API from Your First Worker.
Define an entity
An entity is a class whose decorators describe a table. Create workers/app/source/entities/NoteEntity.ts:
Three rules to notice:
- Extend
OrmTrackingEntity. It gives your entity change tracking (updates only send the fields you actually changed) and the staticfrom(...)constructor you'll use to create rows. - Every column property uses
declare. The base class installs tracking accessors for each column at construction time;declaremakes the property type-only so those accessors survive. A Base lint rule enforces this, so you can't forget it. - Every column states its database type explicitly:
{ kind: 'varchar', length: 255 },{ kind: 'text' }. No inference, no surprises in the generated SQL.@OrmPrimaryAutoColumn('uuid')is a primary key that generates a UUID for you; the create/update date columns maintain themselves.
Configure the database
Tell your worker it has a database. In settings.ts, add an orm block:
The ORM rides on Drizzle underneath. '@default' names this database (a worker can have several); binding: 'DATABASE' is the Cloudflare binding it attaches to; entities lists every entity that lives in it.
Then declare the matching D1 binding in wrangler.toml under the development environment:
For local development wrangler simulates D1 on your machine, so a placeholder database_id is fine; you'll create the real database when you deploy. The migrations_table is named per worker (__drizzle_migrations_<worker>, with - folded to _) so workers sharing a database keep independent migration histories; base check validates that your orm settings and wrangler bindings line up, including that this table name matches the one Base's migration commands use.
Generate and apply the migration
Base derives SQL migrations from your entities, so you never hand-write schema SQL. Generate one:
This diffs your entities against the last known schema and writes a numbered .sql migration into workers/app/database/@default/drizzle/migrations/. Open it and read it: it's the CREATE TABLE you'd expect. Migrations are files in your repo: reviewed in PRs, versioned with your code.
Apply it to your local database:
The --local flag targets wrangler's local D1, the same simulated database base develop reads. Without it, the command applies migrations to the real remote D1, which is exactly what you'll want at deploy time, but not yet.
Two more ORM commands worth knowing as you work: base orm db:studio opens a browser GUI over your database, and base orm db:reset --local wipes local state when you want a clean slate.
Rewrite NoteService against the database
Replace the in-memory array with a repository. Update workers/app/source/services/NoteService.ts:
What changed:
@Injectable()plus a constructor: the service now takes dependencies, so it opts into constructor injection.@InjectRepository(NoteEntity)hands you a typedOrmRepository<NoteEntity>bound to the'@default'database; the entity type flows through, so the repository's methods are fully typed.- Writes are explicit. Build rows with
NoteEntity.from({...}), theninsert; there is nosave-style upsert-by-accident (insert,update,upsert, anddeleteare separate operations). findOne({ where: { id } }): a bare value inwheremeans equality. Richer filters (like,gte,between,inArray, …) are importable functions you'll meet in the how-to guides.- Need more than one entity, or the database itself?
@InjectDatabase()injects the wholeOrmDatabase, anddatabase.getRepository(Entity)returns the same repositories. - The route shapes, the validated input class, and the error handling didn't change at all. The ORM slots into the same service you already had.
Since id is now a UUID string, the () => Number coercion is gone from @HttpPath.
Try it
With the dev server running:
The response now carries a generated UUID id plus createdAt/updatedAt. Restart the dev server, then:
Your note is still there. That's D1 on disk, migrated and typed end to end: entity class → generated SQL → repository → JSON response.
Next: Add GraphQL, which exposes the same notes over a typed GraphQL API in about ten lines.