Skip to Content
BackendBackend Architecture

Backend Architecture

The CardNexus backend is built on Express with oRPC for contract-first API design, MongoDB with Mongoose for data persistence, and TSyringe for dependency injection. This page provides a high-level overview of how everything fits together.

Core Technologies

TechnologyPurpose
ExpressHTTP server and routing
oRPCContract-first, type-safe API layer
MongoDB + MongooseDatabase and ODM
TSyringeDependency injection container
neverthrowType-safe error handling with Result pattern

Layered Architecture

We follow a strict layered architecture to prevent circular dependencies and maintain clean separation of concerns. Each layer can only depend on layers below it.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ LAYER 4: @repo/api (handlers) β”‚ β”‚ Orchestrates domains, enriches data, maps errors to oRPC β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ LAYER 3: Features (packages/backend/features/) β”‚ β”‚ Cross-domain use cases: checkout, import-export, bulk-operations β”‚ β”‚ β†’ Can depend on: Domains, Views, Foundation β”‚ β”‚ β†’ NEVER depend on: Other Features β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ LAYER 2: Domains β”‚ LAYER 2: Views β”‚ β”‚ (packages/backend/domains/) β”‚ (packages/backend/views/ or β”‚ β”‚ Business logic, single domain β”‚ packages/backend/domains/*-view) β”‚ β”‚ β†’ Can depend on: Views, β”‚ β†’ Can depend on: Foundation ONLY β”‚ β”‚ Foundation β”‚ β†’ NEVER depend on: Domains β”‚ β”‚ β†’ NEVER depend on: Other β”‚ β”‚ β”‚ Domains β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ LAYER 1: Foundation β”‚ β”‚ @repo/db, @repo/api-dtos, @repo/errors, @repo/logger, @repo/i18n β”‚ β”‚ β†’ External packages only β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Layer Breakdown

Layer 4: Handlers (@repo/api)

  • Implement oRPC contracts
  • Orchestrate calls to multiple domains/features
  • Enrich responses by combining data from multiple sources
  • Map service errors to oRPC errors

Layer 3: Features (packages/backend/features/)

  • Cross-domain use cases like checkout, import-export, bulk-operations
  • Coordinate multiple domains for user-facing capabilities
  • Can depend on Domains, Views, and Foundation
  • Features must NOT depend on other Features

Layer 2: Domains (packages/backend/domains/)

  • Business logic for a single bounded context
  • Can depend on Views and Foundation only
  • Domains must NOT import other Domains

Layer 2: Views (packages/backend/views/ or *-view packages)

  • Read-only query builders and aggregations
  • Can depend on Foundation only
  • No business logic mutations allowed

Layer 1: Foundation

  • Core shared packages: @repo/db, @repo/api-dtos, @repo/errors, @repo/logger, @repo/i18n
  • Can only depend on external npm packages

Golden Rules

These rules are critical. Violations cause circular dependencies, tight coupling, and maintenance nightmares.

  1. Domains NEVER import other Domains - Cross-domain coordination belongs in Features or Handlers
  2. Views are read-only - No business logic mutations, just query builders
  3. Features orchestrate - They coordinate multiple domains for user-facing capabilities
  4. Handlers enrich - API handlers fetch from multiple services and assemble responses
  5. Domain services emit facts, not presentation - Emit events, don’t send emails directly
  6. Handlers NEVER use models/database directly - Always go through services

Example: Cross-Domain Orchestration

// ❌ WRONG: Domain importing another domain import { InventoryService } from '@repo/inventory' @injectable() class OrderService { constructor(@inject(InventoryService) private inventory: InventoryService) {} async createOrder() { const snapshot = await this.inventory.getProductSnapshot() // VIOLATION! } }
// βœ… CORRECT: Handler orchestrates multiple domains // In @repo/api handler: const inventoryService = container.resolve(InventoryService) const orderService = container.resolve(OrderService) const snapshot = await inventoryService.getProductSnapshot(items) const orders = await orderService.createOrders({ ...data, snapshot })

Common Patterns

Contract-First API (oRPC)

Contracts are defined in @repo/api-dtos, handlers implement them in @repo/api:

// 1. Define contract in @repo/api-dtos export const myEndpointContract = oc .input(MyInputSchema) .output(MyOutputSchema) .errors(myErrors) // 2. Implement handler in @repo/api export const myEndpointHandler = oc .contract(myEndpointContract) .handler(async ({ input, context, errors }) => { // Implementation })

Error Handling (neverthrow)

Services return Result<T, E> instead of throwing exceptions:

async addItem(userId: string, itemId: string): Promise<Result<CartResponse, AddCartError>> { if (!item) return err(CartErr.itemNotFound(itemId)) return ok(cartResponse) }

Dependency Injection (TSyringe)

Always inject actual class objects, not string tokens:

// βœ… CORRECT @injectable() class MyService { constructor(@inject(InventoryService) private inventory: InventoryService) {} } // ❌ WRONG - string tokens cause runtime errors constructor(@inject("InventoryService") private inventory: InventoryService) {}

Package Naming

All packages use the @repo/ prefix in the monorepo:

  • @repo/api - API handlers
  • @repo/api-dtos - API contracts
  • @repo/db - Database models
  • @repo/inventory - Inventory domain
  • @repo/order - Order domain

Where to Find Things

WhatWhere
API Contractspackages/core/api-dtos/src/lib/
API Handlerspackages/backend/api/src/lib/orpc/handlers/
Domain Servicespackages/backend/domains/
Featurespackages/backend/features/
Viewspackages/backend/views/ or packages/backend/domains/*-view/
Database Modelspackages/core/db/src/mongo/

Next Steps

  • See domain-specific documentation for detailed implementation guides
  • Check the marketplace documentation for a real-world example of the architecture in action
  • Review the API contracts in @repo/api-dtos for the complete API surface
Last updated on