Baseleg Docs

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.json without an ADR. strict: true, noUncheckedIndexedAccess, and exactOptionalPropertyTypes are 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. Use unknown and narrow explicitly. If you reach for any, it is a sign the type design needs work.
  • Use typed identifiers. Prefer MemberId over string for 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 Result for 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 Member is a Member, not a User. A Booking is a Booking, not an Appointment or Slot.
  • Verb–noun for use cases. Use cases are named <verb><Noun>: createBooking, groundAircraft, registerMember.
  • Repository interfaces follow the port pattern. BookingRepository is the port; BookingRepositoryD1 is 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.tsbooking.test.ts.
  • No index.ts barrel files inside packages (only at the package root). Deep barrels obscure what a package exports.