Skip to content

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:

  1. API Gateway (apps/gateway) — Single entry point:
  2. Handles authentication locally (session cookies, login/logout, RBAC)
  3. Proxies all other requests to downstream services with user context headers
  4. Rate limiting
  5. WebSocket proxy for Socket.IO with @socket.io/redis-adapter
  6. Health aggregation from all services
  7. Order Service (apps/order-service) — Extracted from monolith:
  8. Orders, Shopify, fulfillment, orchestration, cancellation, product-mappings
  9. Analytics, event-log, audit, notifications, push-notifications, retry-queue
  10. WebSocket gateway (Socket.IO real-time events)
  11. BullMQ event bus integration (publish order events, subscribe to print-job/shipment events)
  12. Internal API endpoints for service-to-service calls
  13. 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-common builds and tests pass
  • libs/gridflock-core builds 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:

  1. Handles authentication locally — session cookies, login/logout, user/role management endpoints
  2. Proxies all other requests to downstream services, attaching user context headers
  3. Rate limits incoming requests
  4. Proxies WebSocket connections to the Order Service (for real-time updates)
  5. 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 gateway succeeds
  • pnpm nx build order-service succeeds
  • pnpm nx run-many -t lint --all passes
  • No TypeScript errors in any project

API Parity (No Breaking Changes)

  • All existing API endpoints return same responses through gateway
  • apps/web works 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=2 works
  • 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, or eslint-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