22 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Project Overview
Xpeditis is a B2B SaaS maritime freight booking and management platform (maritime equivalent of WebCargo). The platform allows freight forwarders to search and compare real-time shipping rates, book containers online, and manage shipments from a centralized dashboard.
MVP Goal: Deliver a minimal but professional product capable of handling 50-100 bookings/month for 10-20 early adopter freight forwarders within 4-6 months.
Architecture
Hexagonal Architecture (Ports & Adapters)
The codebase follows hexagonal architecture principles with clear separation of concerns:
- API Layer (External Adapters): Controllers, validation, auth middleware
- Application Layer (Ports): Use cases (searchRates, createBooking, confirmBooking)
- Domain Layer (Core): Entities (Booking, RateQuote, Carrier, Organization, User)
- Infrastructure Adapters: DB repositories, carrier connectors, email services, storage, Redis cache
Tech Stack
- Frontend: Next.js (TypeScript) with SSR/ISR
- Backend: Node.js API-first (OpenAPI spec)
- Database: PostgreSQL (TypeORM or Prisma)
- Cache: Redis (15 min TTL for spot rates)
- Storage: S3-compatible (AWS S3 or MinIO)
- Auth: OAuth2 + JWT (access 15min, refresh 7 days)
Project Structure (Planned Monorepo)
apps/
frontend/ # Next.js application
backend/ # Node.js API
libs/
domain/ # Core domain entities and business logic
infra/ # Infrastructure configuration
Core Domain Entities
- Organization:
{ id, name, type, scac, address, logo_url, documents[] } - User:
{ id, org_id, role, email, pwd_hash, totp_secret } - RateQuote:
{ id, origin, destination, carrier_id, price_currency, price_value, surcharges[], etd, eta, transit_days, route, availability } - Booking:
{ id, booking_number, user_id, org_id, rate_quote_id, shipper, consignee, containers[], status, created_at, updated_at } - Container:
{ id, booking_id, type, container_number, vgm, temp, seal_number }
Key API Endpoints
Rate Search
POST /api/v1/rates/search: Search shipping rates with origin/destination/container specs- Response includes carrier, pricing, surcharges, ETD/ETA, transit time, route, CO2 emissions
- Cache TTL: 15 minutes (Redis)
- Timeout: 5 seconds per carrier API
Booking Management
POST /api/v1/bookings: Create booking (multi-step workflow)GET /api/v1/bookings/:id: Get booking details- Booking number format:
WCM-YYYY-XXXXXX(6 alphanumeric chars)
Authentication
/auth/register: Email + password (≥12 chars)/auth/login: JWT-based login + OAuth2 (Google Workspace, Microsoft 365)/auth/logout: Session termination/auth/refresh: Token refresh
Critical Integration Points
Carrier Connectors (MVP Priority)
Implement connectors for 3-5 carriers using Strategy pattern:
- Maersk (required)
- MSC
- CMA CGM
- Hapag-Lloyd
- ONE
Each connector must:
- Normalize data to internal schema
- Implement retry logic and circuit breakers
- Respect rate limiting
- Log detailed metrics
- Respond within 5s or fallback gracefully
Cache Strategy
- Preload top 100 trade lanes on startup
- 15-minute TTL for spot rates
- Cache hit target: >90% for common routes
Security Requirements
- TLS 1.2+ for all traffic
- Password hashing: Argon2id or bcrypt (≥12 rounds)
- OWASP Top 10 protection (rate limiting, input validation, CSP headers)
- Audit logs for sensitive actions
- S3 ACLs for compliance documents
- Optional TOTP 2FA
RBAC Roles
- Admin: Full system access
- Manager: Manage bookings and users within organization
- User: Create and view bookings
- Viewer: Read-only access
Performance Targets
- Rate search: <2s for 90% of requests (with cache)
- Dashboard load: <1s for up to 5k bookings
- Carrier API timeout: 5s
- Email confirmation: Send within 3s of booking
- Session auto-logout: 2h inactivity
Development Workflow
Testing Requirements
- Unit tests: Domain logic and use cases
- Integration tests: Carrier connectors and DB repositories
- E2E tests: Complete booking workflow (happy path + 3 common error scenarios)
Email & Notifications
- Templates: MJML format
- Booking confirmation email on successful booking
- Push notifications (if mobile app)
Document Generation
- PDF booking confirmations
- Excel/PDF export for rate search results
Data Requirements
- Port autocomplete: 10k+ ports (IATA/UN LOCODE)
- Multi-currency support: USD, EUR
- Hazmat support: IMO class validation
- Container types: 20', 40', 40'HC, etc.
MVP Roadmap (4-6 months)
Sprint 0 (2 weeks): Repo setup, infrastructure, OpenAPI skeleton, ports/adapters scaffolding
Phase 1 (6-8 weeks): Rate search API + UI, Redis cache, 1-2 carrier connectors, basic auth
Phase 2 (6-8 weeks): Booking workflow, email templates, dashboard, RBAC, organizations
Phase 3 (4-6 weeks): Additional carrier integrations, exports, E2E tests, monitoring, security hardening
Go-to-market (2 weeks): Early adopter onboarding, support, KPI tracking
Important Constraints
- Pre-fetch top 100 trade lanes on application startup
- All carrier API calls must have circuit breakers
- Booking workflow: ≤4 steps maximum
- Session timeout: 2 hours of inactivity
- Rate search pagination: >20 results
- SLA: 95% of rate searches <1s (including cache)
Business KPIs to Track
- Active users (DAU/MAU)
- Bookings per month
- Search-to-booking conversion rate (target ≥3%)
- Average time to create booking
- Carrier API error rates
- Cache hit ratio
- Customer retention at 3 months
Backend Hexagonal Architecture Guidelines (Node.js/TypeScript)
Phase 1: Business Domain Analysis
Domain Identification
- Primary business domain: Maritime freight booking platform
- Core entities: Organization, User, RateQuote, Booking, Container, Carrier
- Business rules:
- Rate quotes expire after 15 minutes (Redis cache)
- Bookings must validate container availability in real-time
- Multi-step booking workflow (≤4 steps)
- RBAC enforcement for all operations
- Carrier API timeout: 5 seconds with fallback
- Use cases: searchRates, createBooking, confirmBooking, manageOrganizations, authenticateUser
Integration Requirements
- External actors: Freight forwarders (users), carriers (API integrations)
- External services: PostgreSQL, Redis, S3, Email (MJML templates), Carrier APIs (Maersk, MSC, CMA CGM, etc.)
- Input interfaces: REST API (OpenAPI), OAuth2 callbacks
- Output interfaces: Database persistence, email notifications, carrier API calls, S3 document storage
Phase 2: Architectural Design
Module Structure
backend/
├── src/
│ ├── domain/ # Pure business logic (NO external dependencies)
│ │ ├── entities/
│ │ │ ├── organization.entity.ts
│ │ │ ├── user.entity.ts
│ │ │ ├── rate-quote.entity.ts
│ │ │ ├── booking.entity.ts
│ │ │ ├── container.entity.ts
│ │ │ ├── carrier.entity.ts
│ │ │ └── index.ts
│ │ ├── value-objects/
│ │ │ ├── email.vo.ts
│ │ │ ├── booking-number.vo.ts
│ │ │ ├── port-code.vo.ts
│ │ │ └── index.ts
│ │ ├── services/
│ │ │ ├── rate-search.service.ts
│ │ │ ├── booking.service.ts
│ │ │ ├── user.service.ts
│ │ │ └── index.ts
│ │ ├── ports/
│ │ │ ├── in/ # API Ports (use cases)
│ │ │ │ ├── search-rates.port.ts
│ │ │ │ ├── create-booking.port.ts
│ │ │ │ ├── manage-user.port.ts
│ │ │ │ └── index.ts
│ │ │ └── out/ # SPI Ports (infrastructure interfaces)
│ │ │ ├── rate-quote.repository.ts
│ │ │ ├── booking.repository.ts
│ │ │ ├── user.repository.ts
│ │ │ ├── carrier-connector.port.ts
│ │ │ ├── cache.port.ts
│ │ │ ├── email.port.ts
│ │ │ └── index.ts
│ │ └── exceptions/
│ │ ├── booking-not-found.exception.ts
│ │ ├── invalid-rate-quote.exception.ts
│ │ ├── carrier-timeout.exception.ts
│ │ └── index.ts
│ │
│ ├── application/ # Controllers and DTOs (depends ONLY on domain)
│ │ ├── controllers/
│ │ │ ├── rates.controller.ts
│ │ │ ├── bookings.controller.ts
│ │ │ ├── auth.controller.ts
│ │ │ └── index.ts
│ │ ├── dto/
│ │ │ ├── rate-search.dto.ts
│ │ │ ├── create-booking.dto.ts
│ │ │ ├── booking-response.dto.ts
│ │ │ └── index.ts
│ │ ├── mappers/
│ │ │ ├── rate-quote.mapper.ts
│ │ │ ├── booking.mapper.ts
│ │ │ └── index.ts
│ │ └── config/
│ │ ├── validation.config.ts
│ │ └── swagger.config.ts
│ │
│ ├── infrastructure/ # All external integrations (depends ONLY on domain)
│ │ ├── persistence/
│ │ │ ├── typeorm/
│ │ │ │ ├── entities/
│ │ │ │ │ ├── organization.orm-entity.ts
│ │ │ │ │ ├── user.orm-entity.ts
│ │ │ │ │ ├── booking.orm-entity.ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── repositories/
│ │ │ │ │ ├── typeorm-booking.repository.ts
│ │ │ │ │ ├── typeorm-user.repository.ts
│ │ │ │ │ └── index.ts
│ │ │ │ └── mappers/
│ │ │ │ ├── booking-orm.mapper.ts
│ │ │ │ └── index.ts
│ │ │ └── database.module.ts
│ │ ├── cache/
│ │ │ ├── redis-cache.adapter.ts
│ │ │ └── cache.module.ts
│ │ ├── carriers/
│ │ │ ├── maersk/
│ │ │ │ ├── maersk.connector.ts
│ │ │ │ ├── maersk.mapper.ts
│ │ │ │ └── maersk.types.ts
│ │ │ ├── msc/
│ │ │ ├── cma-cgm/
│ │ │ └── carrier.module.ts
│ │ ├── email/
│ │ │ ├── mjml-email.adapter.ts
│ │ │ └── email.module.ts
│ │ ├── storage/
│ │ │ ├── s3-storage.adapter.ts
│ │ │ └── storage.module.ts
│ │ └── config/
│ │ ├── database.config.ts
│ │ ├── redis.config.ts
│ │ └── jwt.config.ts
│ │
│ ├── main.ts # Application entry point
│ └── app.module.ts # Root module (NestJS)
│
├── test/
│ ├── unit/
│ ├── integration/
│ └── e2e/
│
├── package.json
├── tsconfig.json
├── jest.config.js
└── .env.example
Port Definitions
API Ports (domain/ports/in/) - Exposed by domain:
SearchRatesPort: Interface for searching shipping ratesCreateBookingPort: Interface for creating bookingsManageUserPort: Interface for user managementAuthenticatePort: Interface for authentication flows
SPI Ports (domain/ports/out/) - Required by domain:
RateQuoteRepository: Persistence interface for rate quotesBookingRepository: Persistence interface for bookingsUserRepository: Persistence interface for usersCarrierConnectorPort: Interface for carrier API integrationsCachePort: Interface for caching (Redis)EmailPort: Interface for sending emailsStoragePort: Interface for S3 document storage
Adapter Design
Driving Adapters (Input):
- REST controllers (NestJS @Controller)
- GraphQL resolvers (future)
- CLI commands (future)
Driven Adapters (Output):
- TypeORM repositories implementing repository ports
- Carrier connectors (Maersk, MSC, etc.) implementing CarrierConnectorPort
- Redis adapter implementing CachePort
- MJML email adapter implementing EmailPort
- S3 adapter implementing StoragePort
Phase 3: Layer Architecture
Domain Layer Rules
- Zero external dependencies: No NestJS, TypeORM, Redis, etc.
- Pure TypeScript: Only type definitions and business logic
- Self-contained: Must compile independently
- Test without framework: Jest only, no NestJS testing utilities
Application Layer Rules
- Depends only on domain: Import from
@domain/*only - Exposes REST API: Controllers validate input and delegate to domain services
- DTO mapping: Transform external DTOs to domain entities
- No business logic: Controllers are thin, logic stays in domain
Infrastructure Layer Rules
- Implements SPI ports: All repository and service interfaces
- Framework dependencies: TypeORM, Redis, AWS SDK, etc.
- Maps external data: ORM entities ↔ Domain entities
- Circuit breakers: Carrier connectors must implement retry/fallback logic
Phase 4: Technical Validation
Dependency Management
domain/package.json (if separate):
{
"dependencies": {}, // NO runtime dependencies
"devDependencies": {
"typescript": "^5.3.0",
"@types/node": "^20.0.0"
}
}
Root package.json:
{
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/swagger": "^7.0.0",
"typeorm": "^0.3.17",
"pg": "^8.11.0",
"redis": "^4.6.0",
"class-validator": "^0.14.0",
"class-transformer": "^0.5.1"
}
}
tsconfig.json Path Aliases
{
"compilerOptions": {
"strict": true,
"baseUrl": "./src",
"paths": {
"@domain/*": ["domain/*"],
"@application/*": ["application/*"],
"@infrastructure/*": ["infrastructure/*"]
}
}
}
Design Patterns
- Domain-Driven Design: Entities, Value Objects, Aggregates
- SOLID Principles: Especially DIP (Dependency Inversion)
- Repository Pattern: Abstraction over data persistence
- Strategy Pattern: Carrier connectors (Maersk, MSC, etc.)
- Circuit Breaker: For carrier API calls (5s timeout)
NestJS Configuration
- Use
@Injectable()in application and infrastructure layers ONLY - Domain services registered manually via
@Moduleproviders - Avoid
@Componentor decorators in domain layer - Use interceptors for transactions (
@Transactional)
Phase 5: Testing Strategy
Unit Tests (Domain)
// domain/services/booking.service.spec.ts
describe('BookingService', () => {
it('should create booking with valid rate quote', () => {
// Test without any framework dependencies
const service = new BookingService(mockRepository);
const result = service.createBooking(validInput);
expect(result.bookingNumber).toMatch(/^WCM-\d{4}-[A-Z0-9]{6}$/);
});
});
Integration Tests (Infrastructure)
// infrastructure/persistence/typeorm/repositories/booking.repository.spec.ts
describe('TypeOrmBookingRepository', () => {
let repository: TypeOrmBookingRepository;
beforeAll(async () => {
// Use testcontainers for real PostgreSQL
await setupTestDatabase();
});
it('should persist booking to database', async () => {
const booking = createTestBooking();
await repository.save(booking);
const found = await repository.findById(booking.id);
expect(found).toBeDefined();
});
});
E2E Tests (Full API)
// test/e2e/booking-workflow.e2e-spec.ts
describe('Booking Workflow (E2E)', () => {
it('should complete full booking flow', async () => {
// 1. Search rates
const ratesResponse = await request(app.getHttpServer())
.post('/api/v1/rates/search')
.send(searchPayload);
// 2. Create booking
const bookingResponse = await request(app.getHttpServer())
.post('/api/v1/bookings')
.send(bookingPayload);
// 3. Verify booking confirmation email sent
expect(bookingResponse.status).toBe(201);
expect(emailSpy).toHaveBeenCalled();
});
});
Test Coverage Targets
- Domain: 90%+ coverage
- Application: 80%+ coverage
- Infrastructure: 70%+ coverage (focus on critical paths)
Phase 6: Naming Conventions
TypeScript Conventions
- Interfaces:
UserRepository(no "I" prefix) - Ports:
SearchRatesPort,CarrierConnectorPort - Adapters:
TypeOrmBookingRepository,MaerskConnectorAdapter - Services:
BookingService,RateSearchService - Entities:
Booking,RateQuote,Organization - Value Objects:
BookingNumber,Email,PortCode - DTOs:
CreateBookingDto,RateSearchRequestDto
File Naming
- Entities:
booking.entity.ts - Interfaces:
booking.repository.ts(for ports) - Implementations:
typeorm-booking.repository.ts - Tests:
booking.service.spec.ts - Barrel exports:
index.ts
Import Order
// 1. External dependencies
import { Injectable } from '@nestjs/common';
// 2. Domain imports
import { Booking } from '@domain/entities';
import { BookingRepository } from '@domain/ports/out';
// 3. Relative imports
import { BookingOrmEntity } from './entities/booking.orm-entity';
Phase 7: Validation Checklist
Critical Questions
- ✅ Domain isolation: No
importof NestJS/TypeORM in domain layer? - ✅ Dependency direction: All dependencies point inward toward domain?
- ✅ Framework-free testing: Can domain be tested without NestJS TestingModule?
- ✅ Database agnostic: Could we switch from TypeORM to Prisma without touching domain?
- ✅ Interface flexibility: Could we add GraphQL without changing domain?
- ✅ Compilation independence: Does domain compile without other layers?
Data Flow Validation
- Inbound: HTTP Request → Controller → DTO → Mapper → Domain Entity → Use Case
- Outbound: Use Case → Repository Port → Adapter → ORM Entity → Database
- Carrier Integration: Use Case → CarrierConnectorPort → MaerskAdapter → Maersk API
Phase 8: Pre-Coding Checklist
Setup Tasks
- ✅ Node.js v20+ installed
- ✅ TypeScript with
strict: true - ✅ NestJS CLI installed globally
- ✅ ESLint + Prettier configured
- ✅ Husky pre-commit hooks
- ✅
.env.examplewith all required variables
Architecture Validation
- ✅ PRD reviewed and understood
- ✅ Domain entities mapped to database schema
- ✅ All ports (in/out) identified
- ✅ Carrier connector strategy defined
- ✅ Cache strategy documented (Redis, 15min TTL)
- ✅ Test strategy approved
Development Order
- Domain layer: Entities → Value Objects → Services → Ports
- Infrastructure layer: TypeORM entities → Repositories → Carrier connectors → Cache/Email adapters
- Application layer: DTOs → Mappers → Controllers → Validation pipes
- Bootstrap: main.ts → app.module.ts → DI configuration
- Tests: Unit (domain) → Integration (repos) → E2E (API)
Common Pitfalls to Avoid
⚠️ Critical Mistakes:
- Circular imports (use barrel exports
index.ts) - Framework decorators in domain (
@Column,@Injectable) - Business logic in controllers or adapters
- Using
anytype (always explicit types) - Promises not awaited (use
async/awaitproperly) - Carrier APIs without circuit breakers
- Missing Redis cache for rate queries
- Not validating DTOs with
class-validator
Recommended Tools
- Validation:
class-validator+class-transformer - Mapping: Manual mappers (avoid heavy libraries)
- API Docs:
@nestjs/swagger(OpenAPI) - Logging: Winston or Pino
- Config:
@nestjs/configwith Joi validation - Testing: Jest + Supertest + @faker-js/faker
- Circuit Breaker:
opossumlibrary - Redis:
ioredis - Email:
mjml+nodemailer
Example: Complete Feature Flow
Scenario: Search Rates for Rotterdam → Shanghai
// 1. Controller (application layer)
@Controller('api/v1/rates')
export class RatesController {
constructor(private readonly searchRatesUseCase: SearchRatesPort) {}
@Post('search')
async searchRates(@Body() dto: RateSearchDto) {
const domainInput = RateSearchMapper.toDomain(dto);
const quotes = await this.searchRatesUseCase.execute(domainInput);
return RateSearchMapper.toDto(quotes);
}
}
// 2. Use Case (domain port in)
export interface SearchRatesPort {
execute(input: RateSearchInput): Promise<RateQuote[]>;
}
// 3. Domain Service (domain layer)
export class RateSearchService implements SearchRatesPort {
constructor(
private readonly cache: CachePort,
private readonly carriers: CarrierConnectorPort[]
) {}
async execute(input: RateSearchInput): Promise<RateQuote[]> {
// Check cache first
const cached = await this.cache.get(input.cacheKey);
if (cached) return cached;
// Query carriers in parallel with timeout
const results = await Promise.allSettled(
this.carriers.map(c => c.searchRates(input))
);
// Filter successful results
const quotes = results
.filter(r => r.status === 'fulfilled')
.flatMap(r => r.value);
// Cache results (15 min TTL)
await this.cache.set(input.cacheKey, quotes, 900);
return quotes;
}
}
// 4. Infrastructure Adapter (infrastructure layer)
export class MaerskConnectorAdapter implements CarrierConnectorPort {
async searchRates(input: RateSearchInput): Promise<RateQuote[]> {
// HTTP call to Maersk API with 5s timeout
const response = await this.httpClient.post(
'https://api.maersk.com/rates',
this.mapToMaerskFormat(input),
{ timeout: 5000 }
);
// Map Maersk response to domain entities
return this.mapToDomainQuotes(response.data);
}
}
This architecture ensures clean separation, testability, and flexibility for the Xpeditis maritime freight platform.