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 domainExample: 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.tsRules
-
No
src/services/directory. Shared infrastructure goes insrc/libs/, database clients insrc/db/, cache insrc/cache/, middleware insrc/middleware/. -
Routes stay thin. Route files in
src/routes/validate input, call module functions, and return responses. Logic belongs in modules. -
No try/catch. The centralized error middleware handles all errors. Throw typed errors and let them bubble up. See the error handling section below.
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:
| Class | Use case |
|---|---|
ApplicationError | Domain failures (not found, bad request, invalid state) |
AuthError | Authentication/session failures |
AuthorizationError | Missing permissions |
ValidationError | Schema validation (usually thrown automatically by the validate middleware) |
The global error middleware formats these into consistent HTTP responses.
Every purchase and sponsorship funds my 8+ years of work on open source given freely to the community. It keeps the lights on, funds new packs, and keeps the ecosystem alive. Even a small tier means a lot. Thank you!
