-
Notifications
You must be signed in to change notification settings - Fork 10
resource and controller layout
Convention for ordering declarations in resource files and controllers. Apply to every **/resources/*.ts and *.Controllers.ts file.
Request classes use the following name patterns. Pick the most specific one that fits.
| Pattern | Use | Examples |
|---|---|---|
List |
Sole list query in the resource. | List |
List* |
Additional list queries; suffix disambiguates. |
ListByCDC, ListLogin, ListOrders
|
Get |
Sole singular query (e.g. by-id). Non-nullable success schema. Missing row → typed NotFoundError (most cases) or Effect.die (only when input is not user-controllable). |
Get |
Get* |
Additional singular queries; suffix names the read. Same non-nullable rule. |
GetById, GetCloseList, GetSettings, GetLabelPreview
|
Find / Find*
|
Singular query that may return null / no result. Absence is part of the normal contract. |
Find, FindByGTIN, FindActiveCart
|
Get vs Find split is non-nullable success vs nullable success. How missing rows surface from a Get depends on who picked the input: typed NotFoundError when the user could plausibly have picked a stale/invalid key, Effect.die only when the input is not user-controllable (tenant enum, own dashboard's own workflow). See query-shape-list-vs-get.md for the full rule.
| <Verb> | Commands. Verb first, alphabetical within commands. | ChangeBlocked, Close, RetryLabel, Update |
Do not name queries with bare nouns (Settings, Orders) or with Preview* prefixes. Use Get*/List*/Find* so the request kind is visible at the call site.
- Imports.
-
// codegen:start ... // codegen:endheader block (containsconst Req = TaggedRequestFor(...)). Do not move. - Request classes, in this order:
List-
List*— alphabetical Get-
Get*— alphabetical Find-
Find*— alphabetical - Commands — alphabetical
- Helper classes (
S.Opaque,S.TaggedStruct,S.TaggedError,S.Class, plainS.Structviews, etc.) live immediately before the first request that references them. If a helper is shared by several requests, place it before the first user in the new order. Helpers not referenced by any request stay grouped near their domain. - Comments above a class travel with that class.
- Trailing
// codegen:start {preset: model} ... // codegen:endblock andexport namespace ...declarations stay at the bottom.
The class body is preserved byte-for-byte during a reorder. No reformatting.
Keep a single public resource module for ordinary consumers, but split query classes into a query-only sibling when command invalidation would otherwise create a circular import.
Pattern:
-
resources/Foo.Queries.tscontains only query request classes and their view schemas. -
resources/Foo.tsre-exportsFoo.Queries.ts, defines the sameReqnamespace, then defines commands. - Commands in other resources import query classes from
Foo.Queries.tsfor invalidation. - Frontend and controllers keep importing
resources/Foounless they only need a query class for resource-level invalidation. - Query-only split files must use the original resource module name in
TaggedRequestFor(...). The shared codegen config strips.Queriesfrommeta-generated module names; if a project does not have that shared default, configurestripSuffixes: [.Queries]at the plugin/config level or on the file. File-levelstripSuffixesoverrides plugin/config defaults.
// resources/PickCarts.Queries.ts
// codegen:start {preset: meta, sourcePrefix: src/EasyLife/}
const Req = TaggedRequestFor("Standard/PickCarts")
// codegen:end
export class List extends Req.Query<List>()("List", {}, {
allowRoles: ["user"],
success: S.Struct({ carts: S.Array(CartState) })
}) {}// resources/PickCarts.ts
import { List as DropshippingPickList } from "../../Dropshipping/resources/PickList.Queries.ts"
import { GetStats } from "./PickCarts.Queries.ts"
export * from "./PickCarts.Queries.ts"
const Req = TaggedRequestFor("Standard/PickCarts")
export class Assign extends Req.Command<Assign>()(
"Assign",
{ cartId: OneOrMoreCarts },
{ allowRoles: ["user"] },
(queryKey) => [queryKey, GetMe, GetStats, DropshippingPickList]
) {}Do not solve circular invalidation by configuring clientFor(Resource, () => …)
in a page. Resource definitions are the source of truth for which query caches a
mutation changes.
Inside return match({ ... }), handler keys follow the same order as the resource. Exactly one blank line between handler blocks; no blank line before the closing }).
Everything outside the match({...}) object (imports, layer deps, *effect setup, helper functions) is untouched.
- Pick the name pattern from the table above.
- Place the class in the resource at the correct slot.
- Place any helper class immediately before its first user in that order.
- Add the matching handler in the controller at the same slot, with a blank line separator.
- Run
pnpm checkfrom the repo root.
For a refactoring pass:
- Identify each top-level
export classblock (request or helper) fromexport classto its terminating}. Comments directly above the class belong to that block. - Classify each request as query (
extends Req.Query<...>) or command (extends Req.Command<...>). - Map each helper to its first-user request by name reference inside the request body.
- Emit blocks in the order above. Verify the count of
extends Req.\(Query\|Command\)matches before and after. - For controllers, reorder handler keys inside
match({...}), insert single blank lines, verify handler count.
- Index
- Import Rules
- Resource & Controller Layout
- Command Pattern
- Command Input Validation
- Query Shape: List vs Get
- Database Query Guidelines
- List Layout
- Streams & Progress
- Vue Conventions
- E2E State Pattern
- E2E
- E2E Toast Wait Audit
- Flow Documentation
- (project-local — create
flows/when first workflow lands)