Some checks failed
CI / Lint & Format Check (push) Failing after 1m11s
CI / Test Backend (push) Failing after 1m32s
CI / Build Backend (push) Has been skipped
Security Audit / npm audit (push) Failing after 5s
Security Audit / Dependency Review (push) Has been skipped
CI / Test Frontend (push) Failing after 29s
CI / Build Frontend (push) Has been skipped
645 lines
22 KiB
Markdown
645 lines
22 KiB
Markdown
# 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 rates
|
||
- `CreateBookingPort`: Interface for creating bookings
|
||
- `ManageUserPort`: Interface for user management
|
||
- `AuthenticatePort`: Interface for authentication flows
|
||
|
||
**SPI Ports (domain/ports/out/)** - Required by domain:
|
||
- `RateQuoteRepository`: Persistence interface for rate quotes
|
||
- `BookingRepository`: Persistence interface for bookings
|
||
- `UserRepository`: Persistence interface for users
|
||
- `CarrierConnectorPort`: Interface for carrier API integrations
|
||
- `CachePort`: Interface for caching (Redis)
|
||
- `EmailPort`: Interface for sending emails
|
||
- `StoragePort`: 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):
|
||
```json
|
||
{
|
||
"dependencies": {}, // NO runtime dependencies
|
||
"devDependencies": {
|
||
"typescript": "^5.3.0",
|
||
"@types/node": "^20.0.0"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Root package.json**:
|
||
```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
|
||
```json
|
||
{
|
||
"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 `@Module` providers
|
||
- Avoid `@Component` or decorators in domain layer
|
||
- Use interceptors for transactions (`@Transactional`)
|
||
|
||
## Phase 5: Testing Strategy
|
||
|
||
### Unit Tests (Domain)
|
||
```typescript
|
||
// 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)
|
||
```typescript
|
||
// 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)
|
||
```typescript
|
||
// 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
|
||
```typescript
|
||
// 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 `import` of 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.example` with 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
|
||
1. **Domain layer**: Entities → Value Objects → Services → Ports
|
||
2. **Infrastructure layer**: TypeORM entities → Repositories → Carrier connectors → Cache/Email adapters
|
||
3. **Application layer**: DTOs → Mappers → Controllers → Validation pipes
|
||
4. **Bootstrap**: main.ts → app.module.ts → DI configuration
|
||
5. **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 `any` type (always explicit types)
|
||
- Promises not awaited (use `async/await` properly)
|
||
- 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/config` with Joi validation
|
||
- **Testing**: Jest + Supertest + @faker-js/faker
|
||
- **Circuit Breaker**: `opossum` library
|
||
- **Redis**: `ioredis`
|
||
- **Email**: `mjml` + `nodemailer`
|
||
|
||
## Example: Complete Feature Flow
|
||
|
||
### Scenario: Search Rates for Rotterdam → Shanghai
|
||
|
||
```typescript
|
||
// 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.
|