Skip to main content

generating

The psy CLI provides three commands for generating Dream models and their accompanying migrations and serializers:

pnpm psy g:resource --help
pnpm psy g:model --help
pnpm psy g:sti-child --help

The resource generator is used when a model will be displayed and/or manipulated via an API endpoint. The model generator is used to generate a model without an API endpoint. And, as the name indicates, the STI child generator is used to generate STI children.

Prefer g:resource for models that will be HTTP-accessible, g:model for models that will never be exposed directly through an API, and g:migration for schema-only changes to existing tables. g:resource and g:model generate id, timestamps, deleted_at, and @SoftDelete() by default; do not pass those columns yourself for new models.

Resource generator commands can be found in the resource generator documentation, and the sti-child has its own, dedicated documentation, but here are some examples of the model and STI child generator:

pnpm psy g:model --no-serializer User email:citext first_name:citext last_name:citext

pnpm psy g:model Guest User:belongs_to

pnpm psy g:model --sti-base-serializer Room type:enum:room_types:Bathroom,Bedroom,Kitchen,Den,LivingRoom Place:belongs_to position:integer:optional

Run psy g:model --help or see the g:model documentation for details of the generator API.

pnpm psy g:model --help

Choosing a generator

In preference order:

  1. g:resource — the default for almost all new models. It is rare for a model to not be exposed via some API — end-user facing, internal, or admin — and g:resource generates the controller, controller specs, and route scaffolding that g:model does not. It's easier to delete unused controller actions later than to retrofit them.
  2. g:sti-child — for STI child models building on an existing STI base.
  3. g:model — only for models that will never be exposed via any API (join tables, audit logs, and the like).
  4. g:migration — for schema-only changes to existing tables, when you're not generating a new model.

If a model has a type column and different types will have different behavior, validations, serializers, or child-specific columns, prefer STI (--sti-base-serializer on the parent resource, then g:sti-child for each child) over hand-rolling the distinction. The STI generators handle scaffolding — check constraints, per-type serializers, type-discriminated OpenAPI schemas — that's much easier to get right at generation time than to refactor in later. See the STI guide.

caution

g:resource unconditionally regenerates the model file, unit spec, factory, and serializer for the given model name — it does not check whether those files already exist, and there's no flag to skip them. Running g:resource again for a model that already has hand-edited associations, hooks, validations, or serializer fields will silently discard those edits. This mainly comes up when a model already exists (from g:model or a prior g:resource) and you need to add the missing controller/routes after the fact: make sure your existing work is committed before re-running it, so you can keep only the newly generated controller, controller spec, route entry, and migration.

After the generator runs

  1. Update the migration file as needed (e.g., add .unique() to a column) before running it.
  2. Run migrations: pnpm psy db:migrate. Under the default test environment this also runs sync automatically — don't follow it with a separate pnpm psy sync in that case; see the migrations guide for the development-environment case.
  3. If it was a resource generator, update the generated controller spec first, then the corresponding controller. Generated action code starts commented out, so a controller spec will hang if an action returns no response.

Enum types

As demonstrated above, you can use the enum type as a column type when specifying your column, provided you give a little bit more information about the enum you are designing. Looking closer at the type field generated for the Room model in the above example, we see some extra information being provided:

type:enum:room_types:Bathroom,Bedroom,Kitchen,Den,LivingRoom

The room_types segment signals to Dream to create a new enum called room_types_enum within your DB. The segment following the room_types segment informs Dream of what values you want to allow for your enum column. In this case, it will produce the following values for your enum:

'Bathroom', 'Bedroom', 'Kitchen', 'Den', 'LivingRoom'

Enum arrays

In some cases, you may need an array of enum values for your column. In this case, simply provide enum[] as the column type segment, instead of enum, like so:

type:enum[]:room_types:Bathroom,Bedroom,Kitchen,Den,LivingRoom

Doing this will produce the same enum type in PostgreSQL, but the column definition will expect an array of enum values instead of a single value. Dream will also set the default value for an enum[] column to be a blank array.

snake_casing column, table, and enum names

Since sql is a case-insensitive language, it is encouraged that you write your columns and tables using snake case. Dream leverages a Kysely setting to automatically convert snake case to camel case so that your Typescript will use the conventional camel case while the database retains the database convention of snake case.

Model organization & namespacing

A model's file path under src/app/models/ becomes its class name, table name, and namespace, so where you place it is a domain-modeling decision, not just a generator mechanic. This is a separate concern from the route path or --owning-model argument a resource is generated with — those control the endpoint and the query scoping, not the model's namespace.

Namespace a model by what it is, not by where it is routed or what owns it. Don't namespace a model under its route or association parent just because the endpoint is nested: a Booking routed at v1/host/places/{}/bookings --owning-model=Place is not Place/Booking. The nested route and --owning-model already give you parent-scoped queries; they say nothing about the model's namespace.

Use a Parent/Child namespace in two cases:

  • STI subtypesRoom/Bedroom, Room/Bathroom. The namespace expresses "is a kind of." See the STI guide.
  • Subdomain / bounded-context modulesReservations/Booking, Billing/Invoice. The namespace expresses which part of the application's domain the model belongs to.

Flat when small, grouped by subdomain when large. A small application is commonly flat — most models sit directly under src/app/models/, and that's fine. As the model set grows, group models into subdomain modules rather than leaving dozens of unrelated top-level peers; a sprawling flat directory is a sign the domain hasn't been carved into bounded contexts yet. Don't pre-create a one-model subdomain on day one either — introduce the module when there are models to put in it. Organize on the subdomain axis, never on the owning model or the route.

A model that belongs to multiple parents is its own aggregate root. When a model belongsTo two parents — a Booking belongs to both a Place and a Guest — it's usually its own organizing concept, not a sub-part of either. Namespacing it under one parent (Place/Booking) wrongly couples a two-parent model to that parent. Leave it top-level, or place it in its subdomain module (Reservations/Booking) — never under one of its parents.

Getting the namespace wrong is expensive to undo: the model file path bakes into the class name, the table name, every import, the serializer/controller paths, and the migration. A later rename touches all of those plus generated types, OpenAPI, and front-end clients. Decide the namespace deliberately at generation time.