Actionable rules and patterns for integrating legacy systems with modern TypeScript/NestJS back-ends.
Your enterprise runs on a maze of legacy systems—COBOL mainframes, SAP ERP, IBM i, and decades-old databases that somehow still power mission-critical operations. You've been tasked with modernizing, but the thought of touching these systems makes you break out in cold sweats. What if there was a way to bridge these worlds without the typical integration nightmare?
Enterprise integration isn't just about connecting systems—it's about navigating a minefield of outdated protocols, undocumented APIs, and systems that were never designed to talk to each other. You're dealing with:
Legacy System Constraints: COBOL systems with no REST APIs, mainframes behind VPN gateways, and databases that predate JSON by decades. Every integration feels like archaeological work.
Data Transformation Chaos: Converting EBCDIC to UTF-8, mapping 40-character fixed-width fields to modern JSON schemas, and handling time zones that nobody documented properly.
Security Compliance Gaps: Legacy systems with weak authentication, no OAuth support, and audit trails that compliance teams hate reviewing.
Operational Brittleness: Integration layers that break when legacy systems have maintenance windows, timeout inconsistencies, and error messages that reveal nothing useful.
Knowledge Silos: The one person who understands the AS/400 system retires next month, and their documentation consists of handwritten notes from 1987.
These Cursor Rules transform how you approach legacy integration by establishing a composable, API-first architecture that treats legacy systems as external services rather than architectural constraints.
Instead of building monolithic integration layers that become unmaintainable, you create independent connector modules for each legacy system. Each connector is a self-contained NestJS module with its own error handling, circuit breakers, and health checks.
// Before: Monolithic integration nightmare
class LegacyIntegration {
async processOrder(order: any) {
// 500+ lines of mixed SAP, mainframe, and business logic
// Impossible to test, maintain, or understand
}
}
// After: Composable connector architecture
@Module({
providers: [SapOrderService, MainframeInventoryService, ModernOrderService],
exports: [OrderOrchestrationService]
})
export class OrderProcessingModule {
// Clean separation of concerns
// Independent testing and deployment
// Clear error boundaries
}
The rules enforce strict boundaries between legacy and modern code—no direct database access from modern services, no legacy business logic bleeding into your clean TypeScript codebase.
Before: Adding a new legacy system integration meant weeks of reverse engineering, custom connection handling, and brittle error management.
After: Generate a new connector in minutes with standardized patterns, built-in resilience, and comprehensive monitoring.
Instead of diving into integration code, you start with a structured assessment that maps out data models, change windows, SLAs, and hidden dependencies. This front-loaded analysis prevents the typical "surprise discoveries" that derail integration projects.
Route new traffic to modern services while proxying existing functionality to legacy systems. No big-bang migrations that keep you up at night wondering what you broke.
// Gradual migration with zero business disruption
@Controller('orders')
export class OrderController {
@Get(':id')
async getOrder(@Param('id') id: string) {
// New orders from modern system
if (await this.isModernOrder(id)) {
return this.modernOrderService.getById(id);
}
// Legacy orders still work
return this.legacyOrderService.getById(id);
}
}
Every legacy connector includes circuit breakers, exponential backoff, and rate limiting. Legacy systems won't bring down your modern infrastructure when they inevitably have issues.
# Generate connector boilerplate
nest generate legacy-connector --name=sap-hr
# Implement required interfaces
export class SapHrConnector implements LegacyConnector<SapEmployee> {
async fetchEmployees(): Promise<SapEmployee[]> {
// Connector-specific logic with built-in error handling
}
}
# Register health check
@Module({
providers: [SapHrConnector, SapHrHealthIndicator]
})
Clean separation between legacy data formats and modern DTOs with dedicated mapper classes:
@Injectable()
export class SapEmployeeMapper {
toModernFormat(sapData: SapEmployeeRecord): Employee {
return {
id: sapData.PERNR.trim(),
name: this.convertFromCp1252(sapData.VORNA + ' ' + sapData.NACHN),
hireDate: this.convertSapDate(sapData.BEGDA)
};
}
}
Comprehensive observability with OpenTelemetry spans, structured logging, and error classification:
// Every legacy call is traced
@Trace('sap.fetch-employees')
async fetchEmployees(): Promise<Employee[]> {
try {
const employees = await this.sapClient.getEmployees();
this.logger.info('SAP employee fetch successful', { count: employees.length });
return employees;
} catch (error) {
if (error instanceof SapTimeoutError) {
this.circuitBreaker.recordFailure();
throw new LegacyTimeoutError();
}
throw error;
}
}
npm install @nestjs/core @nestjs/common class-validator opossum
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"exactOptionalPropertyTypes": true,
"useUnknownInCatchVariables": true
}
}
// src/legacy/mainframe/mainframe.module.ts
@Module({
providers: [MainframeService, MainframeHealthIndicator],
exports: [MainframeService]
})
export class MainframeModule {}
// src/legacy/mainframe/mainframe.service.ts
@Injectable()
export class MainframeService {
private readonly circuitBreaker = new CircuitBreaker(
this.callMainframe.bind(this),
{ timeout: 5000, errorThresholdPercentage: 50 }
);
async getCustomer(id: string): Promise<Customer> {
return this.circuitBreaker.fire(id);
}
}
@Injectable()
export class MainframeHealthIndicator extends HealthIndicator {
async isHealthy(key: string): Promise<HealthIndicatorResult> {
try {
await this.mainframeService.ping();
return this.getStatus(key, true);
} catch (error) {
return this.getStatus(key, false, { error: error.message });
}
}
}
// OAuth 2.1 with PKCE for legacy system access
@Injectable()
export class LegacyAuthService {
async getAccessToken(): Promise<string> {
// Implement OAuth 2.1 client credentials flow
// Cache tokens with appropriate TTL
// Handle token refresh automatically
}
}
Stop treating legacy integration as a necessary evil. With these Cursor Rules, you build integration layers that are as maintainable, testable, and reliable as your modern microservices. Your legacy systems become composable building blocks rather than architectural constraints.
The result? Enterprise integration that actually works—incremental migration paths, built-in resilience, and developer workflows that don't make you dread touching legacy systems.
Ready to transform how your team approaches legacy integration? These rules provide the foundation for integration architecture that scales with your business, not against it.
You are an expert in:
- TypeScript (ES2022 strict mode)
- Node.js 18+
- NestJS 10 (Express adapter)
- REST & GraphQL APIs
- Event-driven microservices (Kafka, RabbitMQ)
- Cloud-native patterns (Docker, Kubernetes, iPaaS, ESB-style messaging)
- Security protocols (OAuth 2.1, OIDC, mTLS)
Key Principles
- Start every project with a short (≤2-week) **Legacy System Assessment** covering data models, change windows, SLAs, and hidden dependencies.
- Follow **API-first** design; legacy code never exposes database directly—only through stable, versioned APIs or message contracts.
- Prefer incremental strangler-fig migration: route new traffic to modern services while proxying untouched routes to legacy.
- Keep the integration layer stateless; persist state in dedicated backing stores or event logs, never in process memory.
- Build composable modules: each connector (e.g., SAP, AS400) is an independent NestJS Module published to a private npm registry.
- Documentation is part of the definition of done: update OpenAPI/GraphQL SDL, sequence diagrams, and run-books with every PR.
- Use **functional, declarative code**; avoid "god" classes. Functions ≤40 LOC, single responsibility.
- Direct database access from modern code allowed only through migration scripts—never from runtime services.
TypeScript Rules
- Enable "strict", "noImplicitAny", "exactOptionalPropertyTypes", "useUnknownInCatchVariables" in tsconfig.
- All files use ES modules; file names in kebab-case, e.g., legacy-sap.module.ts.
- Prefer `interface` for contracts, `type` for unions/intersections.
- Use async/await; forbid top-level `Promise.then` chains via ESLint rule `promise/prefer-await-to-then`.
- Throw *typed* errors extending `AppError` (see Error Handling section) instead of `Error`.
- Use enums only for closed sets (status codes, event types); otherwise string literal unions.
- Private helpers live in `/internal/` folders; exported SDK lives in `/src/public`.
- Example connector skeleton:
```ts
// src/legacy-sap/sap.connector.ts
export interface SapCredentials { user: string; password: string }
export interface SapSalesOrderDto { /* … */ }
export async function fetchSalesOrders(creds: SapCredentials): Promise<SapSalesOrderDto[]> {
const client = createSapClient(creds)
return client.getOrders()
}
```
Error Handling & Validation
- **Fail fast**: validate inbound payloads with `class-validator` pipes as the first middleware.
- Centralise errors in `AppError` hierarchy
```ts
export abstract class AppError extends Error { constructor(readonly code: string, message: string){super(message)} }
export class LegacyTimeoutError extends AppError { constructor(){super('LEGACY_TIMEOUT','Legacy system did not respond within SLA')} }
```
- Use NestJS Exception Filters to map errors to HTTP codes / GraphQL errors.
- Implement exponential back-off + circuit-breaker (`opossum` library) around every legacy call; open circuit on ≥3 consecutive failures.
- Apply global rate limiter (`@nestjs/throttler`) to protect legacy endpoints—configure per-consumer quotas.
- Sanitise error messages—never reveal hostnames, file paths, or stack traces to external consumers.
Framework-Specific (NestJS)
- Module layout:
/src
└── legacy
└── sap
├── sap.module.ts ← exports `SapService`
├── sap.controller.ts ← /api/sap/* façade
└── sap.mapper.ts ← transforms SAP XML ⇄ JSON DTOs
- Use **Interceptors** for: logging (requestId in MDC), response compression, legacy-to-modern data mapping.
- Use **Pipes** for validation/sanitisation; **Guards** for OAuth 2.1 resource checks.
- Always register **Health checks** (TerminusModule) per legacy dependency.
- Provide migration scripts via @nestjs/cli `schematics` to generate connector templates.
Additional Sections
Testing
- **Contract tests first** using Pact; verifies modern service still meets legacy consumer expectations.
- Write integration tests with Jest + Testcontainers to spin up dummy legacy simulators.
- For phased migrations, include **golden sample data tests**: snapshot transformed payloads and diff on CI.
Performance & Resilience
- Enable HTTP/2 where possible for multiplexing.
- Cache read-only reference data (e.g., currencies) in Redis with TTL.
- For bulk sync jobs, use streaming (Node `Readable`) instead of loading full files in memory.
- Tag each external call with OpenTelemetry span attributes: `legacy.system`, `operation`, `duration_ms`.
Security
- Use OAuth 2.1 Authorization Code + PKCE for human users, Client Credentials for machine clients.
- Enforce mTLS between integration layer and legacy VPN gateway.
- Encrypt data in transit (TLS 1.3) and at rest (AES-256, server-side encryption for S3/GCS buckets).
- Classify data; mask PII in logs (`pino` redaction).
Deployment & DevOps
- Container images distroless node:18-slim. Build with multi-stage Dockerfile; final image ≤200 MB.
- Helm chart values include `legacy.timeout`, `circuitBreaker.failureThreshold` for environment tuning.
- Blue/green deploy when toggling traffic from legacy to modern; use progressive percentage rollout.
Documentation
- Auto-generate OpenAPI via `@nestjs/swagger`; publish HTML docs to internal portal on every merge.
- Maintain data mapping sheets (.xlsx) in `/docs/mappings` folder, version-controlled with Git LFS.
- Provide ADRs (Architecture Decision Records) for each major integration decision.
Common Pitfalls & Mitigations
- "Chatty" APIs: batch calls or move to streaming to avoid exhausting legacy session pools.
- Long-running transactions: implement saga pattern; do not hold DB locks across system boundaries.
- Time-zone inconsistencies: normalise to UTC at integration layer.
- Character encoding issues: convert legacy CP1252/Shift-JIS to UTF-8 before persistence.
Ready-to-Use ESLint Excerpt (placed in .eslintrc.js)
```js
module.exports = {
extends: ['@nestjs/eslint-plugin/recommended', 'plugin:@typescript-eslint/recommended'],
rules: {
'@typescript-eslint/explicit-function-return-type': 'error',
'no-console': ['error', { allow: ['warn','error'] }],
'max-lines-per-function': ['warn', 40]
}
}
```
Folder Naming Convention
- `/connectors/{system-name}` for direct SDK wrappers (e.g., connectors/ibm-i, connectors/cobol-db2)
- `/bridges/{protocol}` for protocol translators (e.g., bridges/ftp→s3)
- `/jobs/{frequency}` for schedulers (e.g., jobs/daily/full-sync-orders)
With these rules, teams can modernise legacy estates incrementally while maintaining uptime, security, and developer productivity.