11 KiB
🚀 Next Steps - Getting Started with Development
You've successfully completed Sprint 0! Here's what to do next.
🎯 Immediate Actions (Today)
1. Install Dependencies
# From the root directory
npm install
Expected: This will take 2-3 minutes. You may see some deprecation warnings (normal).
On Windows: If you see EISDIR symlink errors, that's okay - dependencies are still installed.
2. Start Docker Services
docker-compose up -d
Expected: PostgreSQL and Redis containers will start.
Verify:
docker-compose ps
# You should see:
# xpeditis-postgres - Up (healthy)
# xpeditis-redis - Up (healthy)
3. Setup Environment Files
# Backend
cp apps/backend/.env.example apps/backend/.env
# Frontend
cp apps/frontend/.env.example apps/frontend/.env
Note: Default values work for local development. No changes needed!
4. Start the Backend
# Option 1: From root
npm run backend:dev
# Option 2: From backend directory
cd apps/backend
npm run dev
Expected Output:
╔═══════════════════════════════════════╗
║ 🚢 Xpeditis API Server Running ║
║ API: http://localhost:4000/api/v1 ║
║ Docs: http://localhost:4000/api/docs ║
╚═══════════════════════════════════════╝
Verify: Open http://localhost:4000/api/v1/health
5. Start the Frontend (New Terminal)
# Option 1: From root
npm run frontend:dev
# Option 2: From frontend directory
cd apps/frontend
npm run dev
Expected Output:
▲ Next.js 14.0.4
- Local: http://localhost:3000
✓ Ready in 2.3s
Verify: Open http://localhost:3000
✅ Verification Checklist
Before proceeding to development, verify:
npm installcompleted successfully- Docker containers are running (check with
docker-compose ps) - Backend starts without errors
- Health endpoint returns 200 OK: http://localhost:4000/api/v1/health
- Swagger docs accessible: http://localhost:4000/api/docs
- Frontend loads: http://localhost:3000
- No TypeScript compilation errors
All green? You're ready to start Phase 1! 🎉
📅 Phase 1 - Core Search & Carrier Integration (Next 6-8 weeks)
Week 1-2: Domain Layer & Port Definitions
Your first tasks:
1. Create Domain Entities
Create these files in apps/backend/src/domain/entities/:
// organization.entity.ts
export class Organization {
constructor(
public readonly id: string,
public readonly name: string,
public readonly type: 'FREIGHT_FORWARDER' | 'NVOCC' | 'DIRECT_SHIPPER',
public readonly scac?: string,
public readonly address?: Address,
public readonly logoUrl?: string,
) {}
}
// user.entity.ts
export class User {
constructor(
public readonly id: string,
public readonly organizationId: string,
public readonly email: Email, // Value Object
public readonly role: UserRole,
public readonly passwordHash: string,
) {}
}
// rate-quote.entity.ts
export class RateQuote {
constructor(
public readonly id: string,
public readonly origin: PortCode, // Value Object
public readonly destination: PortCode, // Value Object
public readonly carrierId: string,
public readonly price: Money, // Value Object
public readonly surcharges: Surcharge[],
public readonly etd: Date,
public readonly eta: Date,
public readonly transitDays: number,
public readonly route: RouteStop[],
public readonly availability: number,
) {}
}
// More entities: Carrier, Port, Container, Booking
2. Create Value Objects
Create these files in apps/backend/src/domain/value-objects/:
// email.vo.ts
export class Email {
private constructor(private readonly value: string) {
this.validate(value);
}
static create(value: string): Email {
return new Email(value);
}
private validate(value: string): void {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
throw new InvalidEmailException(value);
}
}
getValue(): string {
return this.value;
}
}
// port-code.vo.ts
export class PortCode {
private constructor(private readonly value: string) {
this.validate(value);
}
static create(value: string): PortCode {
return new PortCode(value.toUpperCase());
}
private validate(value: string): void {
// UN LOCODE format: 5 characters (CCCCC)
if (!/^[A-Z]{5}$/.test(value)) {
throw new InvalidPortCodeException(value);
}
}
getValue(): string {
return this.value;
}
}
// More VOs: Money, ContainerType, BookingNumber, DateRange
3. Define Ports
API Ports (domain/ports/in/) - What the domain exposes:
// search-rates.port.ts
export interface SearchRatesPort {
execute(input: RateSearchInput): Promise<RateQuote[]>;
}
export interface RateSearchInput {
origin: PortCode;
destination: PortCode;
containerType: ContainerType;
mode: 'FCL' | 'LCL';
departureDate: Date;
weight?: number;
volume?: number;
hazmat: boolean;
}
SPI Ports (domain/ports/out/) - What the domain needs:
// rate-quote.repository.ts
export interface RateQuoteRepository {
save(rateQuote: RateQuote): Promise<void>;
findById(id: string): Promise<RateQuote | null>;
findByRoute(origin: PortCode, destination: PortCode): Promise<RateQuote[]>;
}
// carrier-connector.port.ts
export interface CarrierConnectorPort {
searchRates(input: RateSearchInput): Promise<RateQuote[]>;
checkAvailability(input: AvailabilityInput): Promise<boolean>;
}
// cache.port.ts
export interface CachePort {
get<T>(key: string): Promise<T | null>;
set<T>(key: string, value: T, ttl: number): Promise<void>;
delete(key: string): Promise<void>;
}
4. Write Domain Tests
// domain/services/rate-search.service.spec.ts
describe('RateSearchService', () => {
let service: RateSearchService;
let mockCache: jest.Mocked<CachePort>;
let mockConnectors: jest.Mocked<CarrierConnectorPort>[];
beforeEach(() => {
mockCache = createMockCache();
mockConnectors = [createMockConnector('Maersk')];
service = new RateSearchService(mockCache, mockConnectors);
});
it('should return cached rates if available', async () => {
const input = createTestRateSearchInput();
const cachedRates = [createTestRateQuote()];
mockCache.get.mockResolvedValue(cachedRates);
const result = await service.execute(input);
expect(result).toEqual(cachedRates);
expect(mockConnectors[0].searchRates).not.toHaveBeenCalled();
});
it('should query carriers if cache miss', async () => {
const input = createTestRateSearchInput();
mockCache.get.mockResolvedValue(null);
const carrierRates = [createTestRateQuote()];
mockConnectors[0].searchRates.mockResolvedValue(carrierRates);
const result = await service.execute(input);
expect(result).toEqual(carrierRates);
expect(mockCache.set).toHaveBeenCalledWith(
expect.any(String),
carrierRates,
900, // 15 minutes
);
});
// Target: 90%+ coverage for domain
});
📚 Recommended Reading Order
Before starting development, read these in order:
-
QUICK-START.md (5 min)
- Get everything running
-
CLAUDE.md (30 min)
- Understand hexagonal architecture
- Learn the rules for each layer
- See complete examples
-
apps/backend/README.md (10 min)
- Backend-specific guidelines
- Available scripts
- Testing strategy
-
TODO.md - Sections relevant to current sprint (20 min)
- Detailed task breakdown
- Acceptance criteria
- Technical specifications
🛠️ Development Guidelines
Hexagonal Architecture Rules
Domain Layer (src/domain/):
- ✅ Pure TypeScript classes
- ✅ Define interfaces (ports)
- ✅ Business logic only
- ❌ NO imports from NestJS, TypeORM, or any framework
- ❌ NO decorators (@Injectable, @Column, etc.)
Application Layer (src/application/):
- ✅ Import from
@domain/*only - ✅ Controllers, DTOs, Mappers
- ✅ Handle HTTP-specific concerns
- ❌ NO business logic
Infrastructure Layer (src/infrastructure/):
- ✅ Import from
@domain/*only - ✅ Implement port interfaces
- ✅ Framework-specific code (TypeORM, Redis, etc.)
- ❌ NO business logic
Testing Strategy
- Domain: 90%+ coverage, test without any framework
- Application: 80%+ coverage, test DTOs and mappings
- Infrastructure: 70%+ coverage, test with test databases
Git Workflow
# Create feature branch
git checkout -b feature/domain-entities
# Make changes and commit
git add .
git commit -m "feat: add Organization and User domain entities"
# Push and create PR
git push origin feature/domain-entities
🎯 Success Criteria for Week 1-2
By the end of Sprint 1-2, you should have:
- All core domain entities created (Organization, User, RateQuote, Carrier, Port, Container)
- All value objects created (Email, PortCode, Money, ContainerType, etc.)
- All API ports defined (SearchRatesPort, CreateBookingPort, etc.)
- All SPI ports defined (Repositories, CarrierConnectorPort, CachePort, etc.)
- Domain services implemented (RateSearchService, BookingService, etc.)
- Domain unit tests written (90%+ coverage)
- All tests passing
- No TypeScript errors
- Code formatted and linted
💡 Tips for Success
1. Start Small
Don't try to implement everything at once. Start with:
- One entity (e.g., Organization)
- One value object (e.g., Email)
- One port (e.g., SearchRatesPort)
- Tests for what you created
2. Test First (TDD)
// 1. Write the test
it('should create organization with valid data', () => {
const org = new Organization('1', 'ACME Freight', 'FREIGHT_FORWARDER');
expect(org.name).toBe('ACME Freight');
});
// 2. Implement the entity
export class Organization { /* ... */ }
// 3. Run the test
npm test
// 4. Refactor if needed
3. Follow Patterns
Look at examples in CLAUDE.md and copy the structure:
- Entities are classes with readonly properties
- Value objects validate in the constructor
- Ports are interfaces
- Services implement ports
4. Ask Questions
If something is unclear:
- Re-read CLAUDE.md
- Check TODO.md for specifications
- Look at the PRD.md for business context
5. Commit Often
git add .
git commit -m "feat: add Email value object with validation"
# Small, focused commits are better
📞 Need Help?
Documentation:
- QUICK-START.md - Setup issues
- CLAUDE.md - Architecture questions
- TODO.md - Task details
- apps/backend/README.md - Backend specifics
Troubleshooting:
- INSTALLATION-STEPS.md - Common issues
Architecture:
- Read the hexagonal architecture guidelines in CLAUDE.md
- Study the example flows at the end of CLAUDE.md
🎉 You're Ready!
Current Status: ✅ Sprint 0 Complete Next Milestone: Sprint 1-2 - Domain Layer Timeline: 2 weeks Focus: Create all domain entities, value objects, and ports
Let's build something amazing! 🚀
Xpeditis MVP - Maritime Freight Booking Platform Good luck with Phase 1!