How to Organize Features

Backend code is organized by domain. Each domain in src/features/ owns its business logic, database access, schemas, and tests.

Structure

src/features/<domain>/
  modules/
    <use-case>/
      index.ts           # Module function
      index.test.ts      # Tests
      schemas/            # Zod schemas for this module
      helpers/            # Module-specific helpers
      emails/             # Email templates (React Email)
  db/
    queries/              # Kysely read queries
    mutations/            # Prisma writes, Kysely transactions
  types/                  # Domain types
  constants/              # Domain constants
  websockets/
    emitters.ts           # wsEmit calls for this domain

Example: projects domain

src/features/projects/
  modules/
    create-project/
      index.ts
      index.test.ts
    get-project/
      index.ts
    delete-project/
      index.ts
    projects-list/
      index.ts
  db/
    queries/
      get-by-id.query.ts
      get-many.query.ts
    mutations/
      create-project.mutation.ts
      update-project.mutation.ts
      delete-project.mutation.ts

Rules

Error handling

Use typed error classes from @backend/middleware/error:

import {
  ApplicationError,
  ApplicationErrorCode,
} from "@backend/middleware/error";

if (!project) {
  throw new ApplicationError({
    code: ApplicationErrorCode.NOT_FOUND,
    message: "Project not found",
    statusCode: 404,
  });
}

Available error classes:

ClassUse case
ApplicationErrorDomain failures (not found, bad request, invalid state)
AuthErrorAuthentication/session failures
AuthorizationErrorMissing permissions
ValidationErrorSchema validation (usually thrown automatically by the validate middleware)

The global error middleware formats these into consistent HTTP responses.