Coding standards
These standards apply to all packages in the Baseleg monorepo. They reflect the architectural goals of the project: clear boundaries, testable business rules, and a codebase that stays legible as it grows.
TypeScript
- Strict mode is required. Do not weaken
tsconfig.jsonwithout an ADR.strict: true,noUncheckedIndexedAccess, andexactOptionalPropertyTypesare the baseline. - Explicit types at public boundaries. Every exported function, class, and value must have an explicit return type. Do not rely on inference at package surfaces.
- Avoid
any. Useunknownand narrow explicitly. If you reach forany, it is a sign the type design needs work. - Use typed identifiers. Prefer
MemberIdoverstringfor entity IDs. This prevents mixing IDs across contexts.
// Correct
export function getAircraft(id: AircraftId): Promise<Result<Aircraft, AircraftNotFound>> { ... }
// Wrong — implicit return type, untyped ID
export function getAircraft(id: string) { ... }
Module boundaries
- Keep modules small and cohesive. Each package should have a single, clear responsibility. Avoid “utility barrel” packages that accumulate unrelated code.
- One entrypoint per package. Each package exports from a single
index.ts. Do not deep-import into a package’s internal files from outside. - No circular dependencies. Packages must follow the dependency direction: UI → Application → Domain → Infrastructure. Shared → nothing.
Domain packages
- No framework imports in domain. Domain packages (
packages/domain/*) must not import from Astro, Cloudflare Workers, Drizzle, or any other runtime/framework. They must be testable with plain Vitest and no special runtime. - No infrastructure imports in domain. Domain must not know about databases, HTTP, or external services.
- Business rules go here. If a rule is enforced in the domain, it is enforced everywhere. If it only lives in a UI handler, it can be bypassed.
Error handling
- Use
Resultfor expected domain failures, not thrown exceptions.Result<T, E>makes failure cases explicit in the type system.- Thrown exceptions are for unexpected/unrecoverable errors.
- Implemented in
@baseleg/shared-result.
// Correct — explicit failure type
type BookingResult = Result<Booking, ConflictError | AircraftGroundedError | MemberNotActiveError>;
// Wrong — silent throw
function createBooking(input: BookingInput): Booking {
if (aircraftGrounded) throw new Error('grounded');
}
Naming
- Use domain language. Names in code should match the ubiquitous language. A
Memberis aMember, not aUser. ABookingis aBooking, not anAppointmentorSlot. - Verb–noun for use cases. Use cases are named
<verb><Noun>:createBooking,groundAircraft,registerMember. - Repository interfaces follow the port pattern.
BookingRepositoryis the port;BookingRepositoryD1is the infrastructure implementation.
File organisation
- One aggregate root / entity per file in domain packages. Do not bundle multiple entity definitions into one file.
- Co-locate tests with source. Test files live next to the source they test:
booking.ts→booking.test.ts. - No
index.tsbarrel files inside packages (only at the package root). Deep barrels obscure what a package exports.