AI Prompt: Part 2 — API Gateway + Order Service Extraction¶
Series: Forma3D.Connect Microservice Decomposition + GridFlock STL Pipeline (Part 2 of 6) Purpose: Create the API Gateway and extract the Order Service from the monolith, establishing the core microservice routing and communication patterns Estimated Effort: 24–30 hours Prerequisites: Part 1 completed (
libs/service-common,libs/gridflock-core, Redis running) Output:apps/gateway(auth, routing, rate limiting, WebSocket proxy) +apps/order-service(all order-domain modules extracted from monolith), both building and passing tests Status: 🚧 TODO Previous Part: Part 1 — Shared Infrastructure & Libraries Next Part: Part 3 — Print Service + Shipping Service
🎯 Mission¶
Create the API Gateway and extract the Order Service from the monolith. After this part, the gateway handles all incoming traffic and proxies it to the Order Service (and later to Print/Shipping/GridFlock services). All existing API endpoints must continue working identically.
What this part delivers:
- API Gateway (
apps/gateway) — Single entry point: - Handles authentication locally (session cookies, login/logout, RBAC)
- Proxies all other requests to downstream services with user context headers
- Rate limiting
- WebSocket proxy for Socket.IO with
@socket.io/redis-adapter - Health aggregation from all services
- Order Service (
apps/order-service) — Extracted from monolith: - Orders, Shopify, fulfillment, orchestration, cancellation, product-mappings
- Analytics, event-log, audit, notifications, push-notifications, retry-queue
- WebSocket gateway (Socket.IO real-time events)
- BullMQ event bus integration (publish order events, subscribe to print-job/shipment events)
- Internal API endpoints for service-to-service calls
- HTTP service clients replacing direct service injection
Important: Until Parts 3–4 are complete, the Print Service and Shipping Service don't exist yet. During this phase, the Order Service may still need to call print/shipping logic directly or stub the HTTP clients. The key is that the architecture is correct — the actual service extraction happens in Part 3.
📌 Prerequisites (Part 1 Completed)¶
Verify these before starting:
-
libs/service-commonbuilds and tests pass -
libs/gridflock-corebuilds and tests pass - Redis container running via Docker Compose
- BullMQ event bus implementation available in
libs/service-common - Internal auth guard available in
libs/service-common - Service client base class available in
libs/service-common - User context middleware available in
libs/service-common
🏗️ Architecture¶
API Gateway Routing Table¶
| External Path | Target Service | Internal Path |
|---|---|---|
POST /api/v1/auth/* |
Gateway (local) | Handled locally |
GET /api/v1/orders/* |
Order Service | Same path |
POST /api/v1/orders/* |
Order Service | Same path |
GET /api/v1/print-jobs/* |
Print Service | Same path |
POST /api/v1/print-jobs/* |
Print Service | Same path |
GET /api/v1/shipments/* |
Shipping Service | Same path |
POST /api/v1/shipments/* |
Shipping Service | Same path |
POST /api/v1/gridflock/* |
GridFlock Service | Same path |
GET /api/v1/gridflock/* |
GridFlock Service | Same path |
GET /api/v1/analytics/* |
Order Service | Same path |
POST /api/v1/webhooks/shopify/* |
Order Service | Same path |
POST /api/v1/webhooks/simplyprint/* |
Print Service | Same path |
POST /api/v1/webhooks/sendcloud/* |
Shipping Service | Same path |
GET /api/v1/product-mappings/* |
Order Service | Same path |
GET /api/v1/events/* |
Order Service | Same path |
GET /health |
Gateway (local) | Aggregates service health |
WS /socket.io |
Order Service | WebSocket proxy |
Note: Until Print Service, Shipping Service, and GridFlock Service are created (Parts 3–4), routes to those services should fall back to the Order Service (which still contains all modules temporarily) or return 503.
Service Boundaries¶
| Service | Modules from Current Monolith |
|---|---|
| Gateway | auth, throttler, versioning |
| Order Service | orders, shopify, fulfillment, orchestration, cancellation, product-mappings, analytics, event-log, audit, notifications, push-notifications, gateway (WebSocket), retry-queue |
📁 Files to Create¶
apps/gateway¶
apps/gateway/
├── src/
│ ├── main.ts
│ ├── app/
│ │ └── app.module.ts
│ ├── auth/
│ │ ├── auth.module.ts
│ │ ├── auth.controller.ts # Login, logout, session endpoints
│ │ ├── auth.service.ts
│ │ ├── guards/
│ │ │ ├── session.guard.ts
│ │ │ └── permissions.guard.ts
│ │ └── decorators/
│ │ ├── current-user.decorator.ts
│ │ ├── public.decorator.ts
│ │ └── require-permissions.decorator.ts
│ ├── proxy/
│ │ ├── proxy.module.ts
│ │ ├── proxy.service.ts # HTTP proxy to downstream services
│ │ └── proxy.middleware.ts # Attaches user context headers
│ ├── routing/
│ │ ├── routing.module.ts
│ │ └── route-config.ts # Service → URL mappings
│ ├── health/
│ │ ├── health.module.ts
│ │ └── health.controller.ts # Aggregates downstream health
│ ├── websocket/
│ │ ├── websocket.module.ts
│ │ └── websocket.gateway.ts # Socket.IO proxy with @socket.io/redis-adapter
│ ├── throttler/
│ │ └── throttler.module.ts
│ └── config/
│ └── configuration.ts
├── Dockerfile
├── project.json
├── tsconfig.app.json
├── webpack.config.js
└── jest.config.ts
apps/order-service¶
apps/order-service/
├── src/
│ ├── main.ts # HTTP server + BullMQ event workers
│ ├── app/
│ │ └── app.module.ts
│ ├── orders/ # From apps/api/src/orders/
│ ├── shopify/ # From apps/api/src/shopify/
│ ├── fulfillment/ # From apps/api/src/fulfillment/
│ ├── orchestration/ # From apps/api/src/orchestration/
│ ├── cancellation/ # From apps/api/src/cancellation/
│ ├── product-mappings/ # From apps/api/src/product-mappings/
│ ├── analytics/ # From apps/api/src/analytics/
│ ├── event-log/ # From apps/api/src/event-log/
│ ├── audit/ # From apps/api/src/audit/
│ ├── notifications/ # From apps/api/src/notifications/
│ ├── push-notifications/ # From apps/api/src/push-notifications/
│ ├── retry-queue/ # From apps/api/src/retry-queue/
│ ├── internal/
│ │ ├── internal.module.ts
│ │ ├── internal.controller.ts # /internal/orders/* endpoints
│ │ └── internal-auth.guard.ts # API key guard
│ ├── events/
│ │ ├── events.module.ts
│ │ ├── event-publisher.service.ts # Publishes to BullMQ
│ │ └── event-subscriber.service.ts # Subscribes to print-job.*, shipment.*
│ ├── gateway/ # From apps/api/src/gateway/ (WebSocket)
│ ├── config/
│ ├── database/
│ ├── tenancy/
│ ├── common/
│ └── observability/
├── Dockerfile
├── project.json
└── jest.config.ts
🔧 Implementation¶
Phase 2: API Gateway (8–10 hours)¶
Priority: Critical | Impact: Very High | Dependencies: Part 1
2.1 Create Gateway App¶
pnpm nx generate @nx/nest:application gateway --directory=apps/gateway
2.2 Gateway Architecture¶
The gateway is a thin NestJS app that:
- Handles authentication locally — session cookies, login/logout, user/role management endpoints
- Proxies all other requests to downstream services, attaching user context headers
- Rate limits incoming requests
- Proxies WebSocket connections to the Order Service (for real-time updates)
- Aggregates health checks from all services
// apps/gateway/src/proxy/proxy.middleware.ts
@Injectable()
export class ProxyMiddleware implements NestMiddleware {
private proxies: Record<string, RequestHandler>;
constructor(
private readonly configService: ConfigService,
private readonly authService: AuthService,
) {
this.proxies = {
'/api/v1/orders': this.createProxy('ORDER_SERVICE_URL'),
'/api/v1/print-jobs': this.createProxy('PRINT_SERVICE_URL'),
'/api/v1/shipments': this.createProxy('SHIPPING_SERVICE_URL'),
'/api/v1/gridflock': this.createProxy('GRIDFLOCK_SERVICE_URL'),
'/api/v1/analytics': this.createProxy('ORDER_SERVICE_URL'),
'/api/v1/product-mappings': this.createProxy('ORDER_SERVICE_URL'),
'/api/v1/events': this.createProxy('ORDER_SERVICE_URL'),
'/api/v1/webhooks/shopify': this.createProxy('ORDER_SERVICE_URL'),
'/api/v1/webhooks/simplyprint': this.createProxy('PRINT_SERVICE_URL'),
'/api/v1/webhooks/sendcloud': this.createProxy('SHIPPING_SERVICE_URL'),
};
}
use(req: Request, res: Response, next: NextFunction): void {
const matchingPath = Object.keys(this.proxies).find(path =>
req.path.startsWith(path)
);
if (matchingPath) {
if (req['user']) {
this.attachUserHeaders(req);
}
return this.proxies[matchingPath](req, res, next);
}
next();
}
private attachUserHeaders(req: Request): void {
const user = req['user'];
req.headers[USER_CONTEXT_HEADERS.USER_ID] = user.userId;
req.headers[USER_CONTEXT_HEADERS.TENANT_ID] = user.tenantId;
req.headers[USER_CONTEXT_HEADERS.USER_EMAIL] = user.email;
req.headers[USER_CONTEXT_HEADERS.USER_ROLES] = user.roles.join(',');
req.headers[USER_CONTEXT_HEADERS.USER_PERMISSIONS] = user.permissions.join(',');
req.headers[USER_CONTEXT_HEADERS.IS_SUPER_ADMIN] = String(user.isSuperAdmin);
}
private createProxy(serviceUrlKey: string): RequestHandler {
const target = this.configService.get<string>(serviceUrlKey);
return createProxyMiddleware({
target,
changeOrigin: true,
});
}
}
2.3 Webhook Pass-Through¶
Webhooks (Shopify, SimplyPrint, Sendcloud) bypass session auth but still need routing:
// Webhook routes are marked as @Public() and proxied directly
// The downstream service handles webhook signature validation
2.4 Health Aggregation¶
// apps/gateway/src/health/health.controller.ts
@Controller('health')
export class HealthController {
constructor(private readonly health: HealthCheckService) {}
@Get()
@Public()
async check() {
return this.health.check([
() => this.http.pingCheck('order-service', process.env.ORDER_SERVICE_URL + '/health'),
() => this.http.pingCheck('print-service', process.env.PRINT_SERVICE_URL + '/health'),
() => this.http.pingCheck('shipping-service', process.env.SHIPPING_SERVICE_URL + '/health'),
() => this.http.pingCheck('gridflock-service', process.env.GRIDFLOCK_SERVICE_URL + '/health'),
() => this.http.pingCheck('redis', 'redis://' + process.env.REDIS_HOST),
]);
}
}
2.5 Redis Session Store (Horizontal Scaling)¶
Gateway sessions MUST be stored in Redis — not in-memory:
// apps/gateway/src/main.ts
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
const redisClient = createClient({
url: process.env.REDIS_URL || 'redis://redis:6379',
});
await redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: process.env.NODE_ENV === 'production', maxAge: 24 * 60 * 60 * 1000 },
}));
2.6 Socket.IO Redis Adapter (Horizontal Scaling)¶
// apps/gateway/src/websocket/websocket.gateway.ts
import { createAdapter } from '@socket.io/redis-adapter';
import { createClient } from 'redis';
const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();
await Promise.all([pubClient.connect(), subClient.connect()]);
io.adapter(createAdapter(pubClient, subClient));
Phase 3: Order Service (16–20 hours)¶
Priority: Critical | Impact: Very High | Dependencies: Part 1
The Order Service is the largest service, containing most of the current business logic.
3.1 Create Order Service App¶
pnpm nx generate @nx/nest:application order-service --directory=apps/order-service
3.2 Move Modules¶
Move these modules from apps/api/src/ to apps/order-service/src/:
| Module | Notes |
|---|---|
orders/ |
Core order CRUD and status management |
shopify/ |
Shopify OAuth, webhooks, catalog, backfill |
fulfillment/ |
Shopify fulfillment creation |
orchestration/ |
Order ↔ print job lifecycle coordination |
cancellation/ |
Order + print job cancellation |
product-mappings/ |
SKU to print file mappings |
analytics/ |
Analytics dashboard data |
event-log/ |
Business event logging |
audit/ |
Security audit logging |
notifications/ |
Email notifications |
push-notifications/ |
PWA push notifications |
gateway/ (WebSocket) |
Socket.IO real-time events |
retry-queue/ |
Retry queue for failed operations |
3.3 Replace EventEmitter2 with Event Bus¶
The orchestration module currently listens to in-process events. Replace with BullMQ event bus:
Before (monolith):
@OnEvent(ORDER_EVENTS.CREATED)
async handleOrderCreated(payload: OrderCreatedPayload) { ... }
@OnEvent(PRINT_JOB_EVENTS.COMPLETED)
async handlePrintJobCompleted(payload: PrintJobCompletedPayload) { ... }
After (microservice):
@Injectable()
export class OrchestrationEventSubscriber implements OnModuleInit {
constructor(
private readonly eventBus: EventBusService,
private readonly orchestrationService: OrchestrationService,
) {}
async onModuleInit(): Promise<void> {
await this.eventBus.subscribe(
SERVICE_EVENTS.PRINT_JOB_COMPLETED,
(event) => this.orchestrationService.handlePrintJobCompleted(event as PrintJobCompletedEvent),
);
await this.eventBus.subscribe(
SERVICE_EVENTS.PRINT_JOB_FAILED,
(event) => this.orchestrationService.handlePrintJobFailed(event as PrintJobFailedEvent),
);
await this.eventBus.subscribe(
SERVICE_EVENTS.SHIPMENT_CREATED,
(event) => this.orchestrationService.handleShipmentCreated(event as ShipmentCreatedEvent),
);
}
}
3.4 Replace Direct Service Injection with HTTP Client¶
The orchestration module currently injects PrintJobsService directly. Replace with HTTP client:
Before: this.printJobsService.create(data)
After: this.printServiceClient.createPrintJobs(tenantId, orderId, lineItems)
Note: Until Print Service exists (Part 3), the PrintServiceClient can temporarily point to the Order Service itself or the modules can remain directly injected. The key is establishing the client interface so the switch in Part 3 is seamless.
3.5 Internal API Endpoints¶
Expose endpoints that other services need:
// apps/order-service/src/internal/internal.controller.ts
@Controller('internal')
@UseGuards(InternalAuthGuard)
export class InternalController {
@Get('orders/:id')
async getOrder(@Param('id') id: string, @Query('tenantId') tenantId: string) { ... }
@Get('orders/shopify/:shopifyOrderId')
async getOrderByShopifyId(...) { ... }
@Patch('orders/:id/tracking')
async updateTracking(...) { ... }
}
3.6 Service Main Entry Point¶
// apps/order-service/src/main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: process.env.GATEWAY_URL,
credentials: true,
});
const port = process.env.PORT || 3001;
await app.listen(port);
Logger.log(`Order Service running on port ${port}`);
}
🧪 Testing Requirements¶
Gateway Tests¶
- Session authentication (login, logout, session expiry)
- Proxy routing to correct services
- User context header propagation (all USER_CONTEXT_HEADERS fields)
- Rate limiting enforcement
- Health aggregation (returns overall status from downstream services)
- WebSocket proxy works for Socket.IO connections
- Webhook routes bypass session auth but still route correctly
Order Service Tests¶
- Controllers — correct HTTP responses, input validation
- Services — business logic, error handling
- Repositories — tenant isolation (compound where clauses)
- Event subscribers — correct processing of incoming BullMQ events
- Internal API — endpoints work with valid internal API key, reject without
- All existing order/shopify/fulfillment/orchestration tests still pass
✅ Validation Checklist¶
Build & Lint¶
-
pnpm nx build gatewaysucceeds -
pnpm nx build order-servicesucceeds -
pnpm nx run-many -t lint --allpasses - No TypeScript errors in any project
API Parity (No Breaking Changes)¶
- All existing API endpoints return same responses through gateway
-
apps/webworks without changes when pointed at gateway - Shopify webhooks still work (routed to Order Service)
- WebSocket real-time updates still work (proxied through gateway)
- Authentication flow (login, session, logout) unchanged
Gateway-Specific¶
- Gateway sessions stored in Redis (
connect-redis), not in-memory - Socket.IO WebSocket gateway uses
@socket.io/redis-adapter - Health endpoint aggregates downstream service health
- Rate limiting works
Order Service-Specific¶
- Order Service runs on port 3001
- Internal endpoints protected by
InternalAuthGuard - BullMQ event subscriptions registered on startup
- All moved modules function identically to monolith
- User context extracted from gateway-provided headers
Horizontal Scaling¶
- Gateway sessions stored in Redis —
docker compose up --scale gateway=2works - Socket.IO events broadcast across replicas via Redis adapter
🚫 Constraints¶
- Preserve all existing API endpoints and behavior (zero breaking changes)
- Propagate user context (tenant, user, roles) from gateway via headers
- Gateway sessions MUST be in Redis, NOT in-memory
- Socket.IO MUST use
@socket.io/redis-adapter - Internal endpoints prefixed with
/internal/and NOT exposed through gateway - No
any,ts-ignore, oreslint-disable - Do NOT modify
apps/web— it should work without changes
📚 Key References¶
- NestJS Microservices: https://docs.nestjs.com/microservices/basics
- http-proxy-middleware: https://github.com/chimurai/http-proxy-middleware
- connect-redis: https://github.com/tj/connect-redis
- @socket.io/redis-adapter: https://socket.io/docs/v4/redis-adapter/
- Current auth module:
apps/api/src/auth/ - Current orchestration:
apps/api/src/orchestration/ - Current domain contracts:
libs/domain-contracts/src/lib/
END OF PART 2
Previous: Part 1 — Shared Infrastructure & Libraries Next: Part 3 — Print Service + Shipping Service + Event Rewiring