---
url: https://docs.forestfuture.dev/d1-record/guide/getting-started.md
description: >-
  From an empty Worker to two related Models in about ten minutes: install
  d1-record, generate Models and their tables, add behavior, and use them in a
  Worker.
---

# Getting started

d1-record turns each table in your [D1](https://developers.cloudflare.com/d1/) database into a Model, so your [Worker](https://developers.cloudflare.com/workers/) works with records instead of rows. In this tutorial you’ll build the start of a small blog: users, and the posts they write.

::: warning Version 0.x
d1-record is before 1.0. Its behavior is tested against real D1, but the API may still change. A breaking change ships in a new minor version, so `^0.1.0` never takes one.
:::

## Prerequisites

You’ll need:

* **A Worker project** with [Wrangler](https://developers.cloudflare.com/workers/wrangler/). `npm create cloudflare@latest` makes one.

* **A D1 database bound as `DB`.** Create one with `npx wrangler d1 create my-app-db`, then add the snippet it prints to your Wrangler config:

  ```jsonc [wrangler.jsonc]
  {
    "d1_databases": [
      { "binding": "DB", "database_name": "my-app-db", "database_id": "<the id it printed>" },
    ],
  }
  ```

* **Node.js 22.13 or later**, for the command line.

## Installing d1-record

```sh
npm install @forestfuture/d1-record
```

## Generating the Models

The generator writes a table’s migration and its Model in one step, like `rails generate model`. Generate users first, since each post belongs to a user:

```sh
npx d1-record g model User 'email:string!:unique' 'isActive:boolean!=true'
npx d1-record g model Post user:references 'title:string!' publishedAt:datetime
```

Each column is `name:type`. A `!` makes it required, `=true` sets a default, `:unique` adds a unique index, and `user:references` links each post to a user. The quotes stop your shell from reading the `!`.

The generator writes these files:

| File                                                        | What it is                                                           |
| ----------------------------------------------------------- | -------------------------------------------------------------------- |
| `migrations/0001_create_users.sql`, `0002_create_posts.sql` | The SQL that creates each table                                      |
| `src/db/schema.ts`                                          | Every table’s columns, generated from the migrations. Don’t edit it. |
| `src/models/application-record.ts`                          | Your app’s base class. It’s yours to edit.                           |
| `src/models/user.ts`, `post.ts`                             | One Model per table                                                  |

Here’s the migration for posts:

```sql \[migrations/0002_create_posts.sql]
CREATE TABLE posts (
  id TEXT PRIMARY KEY NOT NULL,
  user_id TEXT NOT NULL REFERENCES users (id),
  title TEXT NOT NULL,
  published_at DATETIME,
  created_at DATETIME NOT NULL,
  updated_at DATETIME NOT NULL
);
CREATE INDEX index_posts_on_user_id ON posts (user_id);

```

::: rails Compared with Rails
Each `id` is a UUID generated in code, not an auto-incrementing integer, so a user and their posts save together atomically. [Generators](./generators#adding-a-table) covers integer keys.
:::

## Creating the tables

Apply the migrations to your local database:

```sh
npx wrangler d1 migrations apply my-app-db --local
```

Use `--remote` when you deploy.

## Adding behavior

A Model’s attributes come from the generated schema, so a Model only declares behavior. The generated `Post` already belongs to a user:

```ts \[src/models/post.ts]
import { belongsTo } from "@forestfuture/d1-record";
import { ApplicationRecord } from "./application-record";
import { User } from "./user";

export class Post extends ApplicationRecord("posts") {
  user = belongsTo(User);
}

```

The generated `User` is one line:

```ts [src/models/user.ts]
import { ApplicationRecord } from "./application-record";

export class User extends ApplicationRecord("users") {}
```

Give it posts, a scope for active users, and rules for saving:

```ts \[src/models/user.ts]
import { hasMany } from "@forestfuture/d1-record";
import { ApplicationRecord } from "./application-record";
import { Post } from "./post";

export class User extends ApplicationRecord("users") {
  posts = hasMany(Post, { dependent: "destroy" }); // posts.user_id points here

  static active = this.scope((q) => q.where({ isActive: true }));

  static {
    this.validates("email", { presence: true, uniqueness: true });
    this.beforeSave((user) => {
      user.email = user.email.toLowerCase();
    });
  }
}

```

* `user.posts()` finds the user’s posts, and `dependent: "destroy"` destroys them with the user.
* `User.active()` finds active users.
* `validates` and `beforeSave` run on every save. An invalid user isn’t saved, and `user.errors` says why.

## Using the Models

Queries start from the Model. Each Model uses the `DB` binding, so there’s nothing to connect:

```ts \[src/index.ts]
import { User } from "./models/user";

export default {
  async fetch(request: Request): Promise<Response> {
    // GET: the 20 newest active users, each with their posts.
    if (request.method === "GET") {
      const users = await User.active()
        .includes("posts") // loaded in one extra query, not one per user
        .order({ createdAt: "desc" })
        .limit(20)
        .toArray();
      return Response.json(users);
    }

    // POST: sign up a user with a first post, saved together.
    const { email } = await request.json<{ email: string }>();
    const user = User.build({ email });
    user.posts().build({ title: "Hello, D1" });

    // save() validates and runs callbacks; false means it wasn't saved, and why is in errors.
    if (!(await user.save())) {
      return Response.json({ errors: user.errors.fullMessages() }, { status: 422 });
    }
    return Response.json(user, { status: 201 });
  },
} satisfies ExportedHandler<Env>;

```

* `includes("posts")` loads every user’s posts in one more query.
* A post built with `user.posts().build(…)` is saved with its user, in one atomic batch.
* `save()` resolves to `false` when validation fails.

## Trying it

Start the Worker:

```sh
npx wrangler dev
```

In another terminal, sign up a user, then list the users:

```sh
curl -X POST http://localhost:8787 -d '{"email":"Ada@Example.com"}'
curl http://localhost:8787
```

The list includes Ada, with her email lowercased by `beforeSave`. Sign her up again, and you’ll get a `422` with `["Email has already been taken"]`.

## Next steps

* **[Models](./models):** attributes, types, and overrides.
* **[Querying](./querying):** conditions, ordering, scopes, and preloading.
* **[Persistence](./persistence):** saving and destroying records.
* **[Associations](./associations):** `hasMany`, `belongsTo`, and `hasOne`.
* **[Generators](./generators):** every way to write columns.
* **[Bulk writes and batches](./batches):** writing several changes at once.
