Express: setting up and testing Express apps
# Express
> Using Scenarist with Express
## Testing Express with Scenarist Scenarist provides first-class support for testing Express applications at the HTTP level, enabling you to test middleware, route handlers, and external API integrations with real HTTP requests. ### The Challenge Testing Express applications traditionally requires choosing between: * Unit testing route handlers in isolation (misses middleware integration) * Mocking Express request/response objects (creates distance from production) * Full E2E tests with browser automation (too slow for comprehensive coverage) ### How Scenarist Helps Scenarist enables HTTP-level integration testing for Express: * Test middleware chains with real HTTP requests * Verify route handlers with different external API scenarios * Test error handling and edge cases comprehensively * Run parallel tests without interference ## Working Example See Scenarist in action with a complete Express application: [**Explore the Express Example App →**](/frameworks/express/example-app/) The example demonstrates: * Testing Express middleware and route handlers * Test ID isolation for parallel execution * Request matching for content-based responses * Sequences for polling and async operations * Stateful mocks with state capture and injection * Complete installation and usage instructions **[View source on GitHub →](https://github.com/citypaul/scenarist/tree/main/apps/express-example)** ## Getting Started Ready to integrate Scenarist into your Express application? [Get started with Express →](/frameworks/express/getting-started/)
# Express Example App
> Working example demonstrating Scenarist with Express
## Overview The Express example demonstrates HTTP-level integration testing for Express applications using Scenarist. This example focuses on testing middleware, route handlers, and external API integrations. **GitHub:** [apps/express-example](https://github.com/citypaul/scenarist/tree/main/apps/express-example) ## What It Demonstrates This example app showcases all major Scenarist features with Express: ### Core Features * **Middleware Testing** - Test Express middleware chains with real HTTP requests * **Route Handlers** - Test route handlers with different external API responses * **Test ID Isolation** - Parallel test execution without interference * **Runtime Scenario Switching** - Switch scenarios during test execution ### Dynamic Response Features * **Request Matching** - Different responses based on request content * **Sequences** - Multi-step processes (polling, async operations) * **Stateful Mocks** - State capture and injection across requests * **Default Fallback** - Graceful handling when no scenario is active ## Installation ### Prerequisites * Node.js 22+ * pnpm 11+ ### Clone and Install
```bash
# Clone the repository
git clone https://github.com/citypaul/scenarist.git
cd scenarist
# Install dependencies
pnpm install
# Navigate to Express example
cd apps/express-example
```
## Running the Example ### Development Mode
```bash
# Start the Express server
pnpm dev
```
Server runs on . ### Run Tests
```bash
# Run all tests
pnpm test
# Run specific test file
pnpm test scenario-switching
# Run with coverage
pnpm test --coverage
```
## Key Files ### Scenarist Setup **`src/app.ts`** - Express app with Scenarist integration (simplified; [view on GitHub](https://github.com/citypaul/scenarist/blob/main/apps/express-example/src/app.ts))
```typescript
import express from "express";
import {
createScenarist,
type ExpressScenarist,
} from "@scenarist/express-adapter";
import { scenarios } from "./scenarios.js";
export const createApp = () => {
const app = express();
app.use(express.json());
// Create Scenarist instance (synchronous - returns undefined in production builds)
const scenarist = createScenarist({
enabled: true,
scenarios,
strictMode: false,
});
// Register Scenarist middleware (skipped in production builds)
if (scenarist) {
app.use(scenarist.middleware);
}
// Your routes (the app registers GitHub, weather, Stripe, cart, form and more)
app.get("/api/github/user/:username", async (req, res) => {
const response = await fetch(
`https://api.github.com/users/${encodeURIComponent(req.params.username)}`,
);
const data = await response.json();
res.status(response.status).json(data);
});
return { app, scenarist };
};
```
**`src/server.ts`** - Entry point
```typescript
import { createApp } from "./app.js";
const { app, scenarist } = createApp();
scenarist?.start();
app.listen(process.env.PORT || 3000);
```
### Scenario Definitions **`src/scenarios.ts`** - All scenario definitions ([view on GitHub](https://github.com/citypaul/scenarist/blob/main/apps/express-example/src/scenarios.ts)) Key scenarios: **`default`** - Standard responses for all tests **`github-not-found`** - GitHub user lookup returns 404
```typescript
{
id: "github-not-found",
name: "GitHub User Not Found",
description: "GitHub API returns 404 for user lookup",
mocks: [{
method: "GET",
url: "https://api.github.com/users/:username",
response: {
status: 404,
body: { message: "Not Found", documentation_url: "https://docs.github.com" }
}
}]
}
```
**`github-polling`** - Polling sequence
```typescript
{
id: "github-polling",
name: "GitHub Job Polling Sequence",
description: "Simulates async GitHub job polling with state progression",
mocks: [{
method: "GET",
url: "https://api.github.com/users/:username",
sequence: {
responses: [
{ status: 200, body: { status: "pending", progress: 0, login: "user1" } },
{ status: 200, body: { status: "processing", progress: 50, login: "user2" } },
{ status: 200, body: { status: "complete", progress: 100, login: "user3" } }
],
repeat: "last"
}
}]
}
```
**`shoppingCart`** - Stateful shopping cart
```typescript
{
id: "shoppingCart",
name: "Shopping Cart (Stateful)",
description: "Stateful shopping cart with capture and injection",
mocks: [
{
method: "GET",
url: "http://localhost:3001/cart",
response: {
status: 200,
body: {
items: "{{state.cartItems}}",
count: "{{state.cartItems.length}}",
total: 0
}
}
},
{
method: "PATCH",
url: "http://localhost:3001/cart",
captureState: {
cartItems: "body.items"
},
response: {
status: 200,
body: {
items: "{{state.cartItems}}",
count: "{{state.cartItems.length}}",
message: "Item added to cart"
}
}
}
]
}
```
### Test Examples The tests share a `createTestFixtures()` helper from `tests/test-helpers.ts` that calls `createApp()`, starts MSW, and returns an async `cleanup()` that stops it. **`tests/scenario-switching.test.ts`** - Basic scenario switching
```typescript
import { SCENARIST_TEST_ID_HEADER } from "@scenarist/express-adapter";
import { describe, it, expect, afterAll } from "vitest";
import request from "supertest";
import { createTestFixtures } from "./test-helpers.js";
const fixtures = createTestFixtures();
describe("Scenario Switching E2E", () => {
afterAll(async () => {
await fixtures.cleanup();
});
it("should switch to success scenario via POST /__scenario__", async () => {
await request(fixtures.app)
.post(fixtures.scenarist.config.endpoints.setScenario)
.set(SCENARIST_TEST_ID_HEADER, "success-test-1")
.send({ scenario: "success" });
// Request uses success scenario
const response = await request(fixtures.app)
.get("/api/github/user/testuser")
.set(SCENARIST_TEST_ID_HEADER, "success-test-1");
expect(response.status).toBe(200);
});
});
```
**`tests/test-id-isolation.test.ts`** - Parallel test isolation
```typescript
describe("Test ID Isolation E2E", () => {
it("should allow different test IDs to use different scenarios concurrently", async () => {
// Test ID 1 uses success
await request(fixtures.app)
.post(fixtures.scenarist.config.endpoints.setScenario)
.set(SCENARIST_TEST_ID_HEADER, "test-id-1")
.send({ scenario: "success" });
// Test ID 2 uses github-not-found
await request(fixtures.app)
.post(fixtures.scenarist.config.endpoints.setScenario)
.set(SCENARIST_TEST_ID_HEADER, "test-id-2")
.send({ scenario: "github-not-found" });
// Requests are isolated
const res1 = await request(fixtures.app)
.get("/api/github/user/testuser")
.set(SCENARIST_TEST_ID_HEADER, "test-id-1");
expect(res1.status).toBe(200);
expect(res1.body.login).toBe("testuser");
const res2 = await request(fixtures.app)
.get("/api/github/user/testuser")
.set(SCENARIST_TEST_ID_HEADER, "test-id-2");
expect(res2.status).toBe(404);
});
});
```
**`tests/dynamic-sequences.test.ts`** - Polling with sequences
```typescript
describe("Dynamic Response Sequences E2E (Phase 2)", () => {
it("should return responses in sequence order (pending → processing → complete)", async () => {
await request(fixtures.app)
.post(fixtures.scenarist.config.endpoints.setScenario)
.set(SCENARIST_TEST_ID_HEADER, "polling-test-1")
.send({ scenario: "github-polling" });
// First request: pending
const res1 = await request(fixtures.app)
.get("/api/github/user/testuser")
.set(SCENARIST_TEST_ID_HEADER, "polling-test-1");
expect(res1.body.status).toBe("pending");
// Second request: processing
const res2 = await request(fixtures.app)
.get("/api/github/user/testuser")
.set(SCENARIST_TEST_ID_HEADER, "polling-test-1");
expect(res2.body.status).toBe("processing");
// Third request: complete
const res3 = await request(fixtures.app)
.get("/api/github/user/testuser")
.set(SCENARIST_TEST_ID_HEADER, "polling-test-1");
expect(res3.body.status).toBe("complete");
});
});
```
**`tests/stateful-scenarios.test.ts`** - State capture and injection
```typescript
describe("Stateful Scenarios E2E (Phase 3)", () => {
it("should capture items and inject into cart response", async () => {
await request(fixtures.app)
.post(fixtures.scenarist.config.endpoints.setScenario)
.set(SCENARIST_TEST_ID_HEADER, "cart-test-1")
.send({ scenario: "shoppingCart" });
// Add items - the route PATCHes the cart, state captured
await request(fixtures.app)
.post("/api/cart/add")
.set(SCENARIST_TEST_ID_HEADER, "cart-test-1")
.send({ item: "Apple" });
await request(fixtures.app)
.post("/api/cart/add")
.set(SCENARIST_TEST_ID_HEADER, "cart-test-1")
.send({ item: "Banana" });
// Get cart - state injected
const response = await request(fixtures.app)
.get("/api/cart")
.set(SCENARIST_TEST_ID_HEADER, "cart-test-1");
expect(response.body.items).toEqual(["Apple", "Banana"]);
});
});
```
## Architecture ### How It Works 1. **Middleware Registration** - Scenarist middleware added to Express app 2. **Test ID Extraction** - Middleware extracts `x-scenarist-test-id` header from requests 3. **Scenario Activation** - Test calls `POST /__scenario__` to set active scenario 4. **Request Handling** - Express routes execute normally 5. **External API Interception** - MSW intercepts external API calls 6. **Scenario Response** - Returns response defined in scenario ### Middleware Flow
```plaintext
Incoming Request
↓
[Scenarist Middleware] - Extracts x-scenarist-test-id header
↓
[Express Route] - Executes normally
↓
[External API Call] - fetch('https://api.example.com/...')
↓
[MSW Intercepts] - Checks active scenario for test ID
↓
[Scenario Response] - Returns mocked response
↓
[Express Route] - Continues with mocked data
↓
Response to Client
```
### File Structure ## Common Patterns ### Testing Middleware
```typescript
// Middleware that fetches user from external API
app.use(async (req, res, next) => {
const response = await fetch("https://api.auth.example.com/user");
req.user = await response.json();
next();
});
// Test with different user scenarios
describe("Auth Middleware", () => {
it("should set premium user", async () => {
const testId = "test-premium";
await request(app)
.post("/__scenario__")
.set("x-scenarist-test-id", testId)
.send({ scenario: "premiumUser" });
const response = await request(app)
.get("/api/profile")
.set("x-scenarist-test-id", testId);
expect(response.body.user.tier).toBe("premium");
});
});
```
### Testing with Request Matching
```typescript
import type { ScenaristScenarios } from "@scenarist/express-adapter";
// Different responses based on request body
const scenarios = {
checkout: {
id: "checkout",
name: "Checkout",
description: "Charge response depends on the amount",
mocks: [
{
method: "POST",
url: "https://api.payment.example.com/charge",
match: { body: { amount: 100 } },
response: { status: 200, body: { success: true } },
},
{
method: "POST",
url: "https://api.payment.example.com/charge",
match: { body: { amount: 10000 } },
response: { status: 400, body: { error: "Amount too large" } },
},
],
},
} as const satisfies ScenaristScenarios;
```
### Testing Default Fallback
```typescript
describe("Default Fallback", () => {
it("should use default scenario when none specified", async () => {
// No scenario switch - uses default
const response = await request(app).get("/api/user");
expect(response.status).toBe(200);
expect(response.body.tier).toBe("standard"); // default scenario
});
});
```
## Next Steps * [Express Getting Started →](/frameworks/express/getting-started/) - Integrate Scenarist into your Express app * [Request Matching →](/scenarios/request-matching/) - Learn about request content matching * [Sequences →](/scenarios/response-sequences/) - Learn about response sequences * [Stateful Mocks →](/scenarios/stateful-mocks/) - Learn about state capture and injection
# Express - Getting Started
> Set up Scenarist with Express in 5 minutes
Test your Express APIs with runtime scenario switching. Zero boilerplate setup using AsyncLocalStorage. ## Installation
```bash
npm install @scenarist/express-adapter
npm install -D msw @playwright/test @scenarist/playwright-helpers
```
`msw` (v2) is a required peer dependency of `@scenarist/express-adapter`. ## Basic Setup **1. Define your scenarios:** src/scenarios.ts
```typescript
import type {
ScenaristScenario,
ScenaristScenarios,
} from "@scenarist/express-adapter";
// ✅ RECOMMENDED - Default scenario with complete happy path
const defaultScenario: ScenaristScenario = {
id: "default",
name: "Happy Path",
description: "All external APIs succeed with valid responses",
mocks: [
// Stripe: Successful payment
{
method: "POST",
url: "https://api.stripe.com/v1/charges",
response: {
status: 200,
body: { id: "ch_123", status: "succeeded", amount: 5000 },
},
},
// Auth0: Authenticated user
{
method: "GET",
url: "https://api.auth0.com/userinfo",
response: {
status: 200,
body: { sub: "user_123", email: "john@example.com", tier: "standard" },
},
},
// SendGrid: Email sent successfully
{
method: "POST",
url: "https://api.sendgrid.com/v3/mail/send",
response: {
status: 202,
body: { message_id: "msg_123" },
},
},
],
};
// Specialized scenario: Override ONLY Stripe for payment failure
const cardDeclinedScenario: ScenaristScenario = {
id: "cardDeclined",
name: "Card Declined",
description: "Stripe declines payment, everything else succeeds",
mocks: [
// Override: Stripe declines payment
{
method: "POST",
url: "https://api.stripe.com/v1/charges",
response: {
status: 402,
body: {
error: { code: "card_declined", message: "Your card was declined" },
},
},
},
// Auth0 and SendGrid automatically fall back to default (happy path)
],
};
export const scenarios = {
default: defaultScenario,
cardDeclined: cardDeclinedScenario,
} as const satisfies ScenaristScenarios;
```
**2. Set up Scenarist in your Express app:** src/app.ts
```typescript
import express from "express";
import {
createScenarist,
type ExpressScenarist,
} from "@scenarist/express-adapter";
import { scenarios } from "./scenarios";
// Use async factory pattern for Express apps
export const createApp = async () => {
const app = express();
app.use(express.json());
// Returns undefined when enabled is false or NODE_ENV is production
const scenarist = createScenarist({
enabled: process.env.NODE_ENV === "test",
scenarios,
});
// Add Scenarist middleware BEFORE your routes (scenarist is undefined in production builds)
if (scenarist) {
app.use(scenarist.middleware);
}
// Your routes run normally - Scenarist just mocks external APIs
app.post("/api/checkout", async (req, res) => {
const { amount, token } = req.body;
// Your validation runs
if (amount < 1) {
return res.status(400).json({ error: "Invalid amount" });
}
// External Stripe call is mocked by Scenarist
const charge = await fetch("https://api.stripe.com/v1/charges", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.STRIPE_KEY}` },
body: JSON.stringify({ amount, source: token }),
});
const result = await charge.json();
// Your business logic runs
if (charge.status === 200) {
return res.json({ success: true, chargeId: result.id });
} else {
return res.status(402).json({ error: result.error.message });
}
});
return { app, scenarist };
};
// src/server.ts - Entry point
const main = async () => {
const { app, scenarist } = await createApp();
if (scenarist) {
scenarist.start();
}
app.listen(3000, () => console.log("Server running on :3000"));
};
main().catch(console.error);
```
**3. Install testing dependencies:**
```bash
npm install -D vitest supertest @types/supertest
```
**4. Write your tests:** tests/checkout.test.ts
```typescript
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import request from "supertest";
import { SCENARIST_TEST_ID_HEADER } from "@scenarist/express-adapter";
import { createApp } from "../src/app";
// Factory function for test setup - no let variables
const createTestSetup = async () => {
const { app, scenarist } = await createApp();
return { app, scenarist };
};
describe("Checkout API", () => {
const testContext = createTestSetup();
beforeAll(async () => {
const { scenarist } = await testContext;
scenarist?.start(); // Start MSW
});
afterAll(async () => {
const { scenarist } = await testContext;
await scenarist?.stop(); // Stop MSW
});
it("processes payment successfully", async () => {
const { app, scenarist } = await testContext;
// Switch to default scenario
await request(app)
.post(scenarist!.config.endpoints.setScenario)
.set(SCENARIST_TEST_ID_HEADER, "test-1")
.send({ scenario: "default" });
// Make API request - your Express route runs normally
const response = await request(app)
.post("/api/checkout")
.set(SCENARIST_TEST_ID_HEADER, "test-1")
.send({ amount: 5000, token: "tok_test" });
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.chargeId).toBe("ch_123");
});
it("handles card declined error", async () => {
const { app, scenarist } = await testContext;
// Switch to cardDeclined scenario
await request(app)
.post(scenarist!.config.endpoints.setScenario)
.set(SCENARIST_TEST_ID_HEADER, "test-2")
.send({ scenario: "cardDeclined" });
const response = await request(app)
.post("/api/checkout")
.set(SCENARIST_TEST_ID_HEADER, "test-2")
.send({ amount: 5000, token: "tok_test" });
expect(response.status).toBe(402);
expect(response.body.error).toContain("declined");
});
});
```
## Test ID Headers **Every test request must include a test ID header** for proper test isolation. This is what enables parallel test execution without interference. ### How Test Isolation Works 1. **Unique test ID per test:** Each test gets a unique identifier (e.g., `'test-1'`, `'test-2'`) 2. **Header on every request:** Send the test ID with every HTTP request 3. **AsyncLocalStorage tracking:** Express adapter uses AsyncLocalStorage to track which test ID is active for the current request 4. **Scenario isolation:** Each test ID has its own active scenario and state ### Required Headers Include the test ID header on **both** scenario switch requests AND actual API requests:
```typescript
// Step 1: Switch scenario (with test ID header)
await request(app)
.post(scenarist.config.endpoints.setScenario)
.set(SCENARIST_TEST_ID_HEADER, "test-1") // ← Test ID header
.send({ scenario: "cardDeclined" });
// Step 2: Make API request (with SAME test ID header)
const response = await request(app)
.post("/api/checkout")
.set(SCENARIST_TEST_ID_HEADER, "test-1") // ← Same test ID
.send({ amount: 5000, token: "tok_test" });
```
**What happens:** 1. First request switches to `'cardDeclined'` scenario for test ID `'test-1'` 2. Second request includes same test ID header 3. AsyncLocalStorage associates the request with `'test-1'` 4. Scenarist uses the `'cardDeclined'` scenario for this request 5. Different test using `'test-2'` would use its own scenario ### Header Name The header name is standardized to `'x-scenarist-test-id'`:
```typescript
// These are equivalent:
.set('x-scenarist-test-id', 'test-1')
.set(SCENARIST_TEST_ID_HEADER, 'test-1') // Recommended
```
**Why use `SCENARIST_TEST_ID_HEADER`?** * Avoids magic strings (clear intent) * Type-safe (TypeScript will catch typos) * Consistent across all Scenarist packages ### Parallel Test Execution Test IDs enable **parallel test execution** without interference:
```typescript
describe("Parallel checkout tests", () => {
it("test 1: successful payment", async () => {
// Uses test ID 'test-1'
await request(app)
.post(scenarist.config.endpoints.setScenario)
.set(SCENARIST_TEST_ID_HEADER, "test-1")
.send({ scenario: "default" });
const response = await request(app)
.post("/api/checkout")
.set(SCENARIST_TEST_ID_HEADER, "test-1")
.send({ amount: 5000, token: "tok_test" });
expect(response.status).toBe(200);
});
it("test 2: card declined", async () => {
// Uses test ID 'test-2' - runs simultaneously with test 1
await request(app)
.post(scenarist.config.endpoints.setScenario)
.set(SCENARIST_TEST_ID_HEADER, "test-2")
.send({ scenario: "cardDeclined" });
const response = await request(app)
.post("/api/checkout")
.set(SCENARIST_TEST_ID_HEADER, "test-2")
.send({ amount: 5000, token: "tok_test" });
expect(response.status).toBe(402);
});
});
```
**Both tests run in parallel:** * Test 1 uses scenario `'default'` via test ID `'test-1'` * Test 2 uses scenario `'cardDeclined'` via test ID `'test-2'` * No interference because test IDs isolate scenarios and state ### Missing Headers = Default Scenario If you forget to include the test ID header:
```typescript
// ❌ Missing test ID header
const response = await request(app)
.post("/api/checkout")
// .set(SCENARIST_TEST_ID_HEADER, 'test-1') ← MISSING!
.send({ amount: 5000, token: "tok_test" });
```
**What happens:** * Request uses the `'default'` scenario (fallback behavior) * If you switched to a different scenario, it won’t be used * May cause confusing test failures (wrong scenario active) **Always include the header on every request!** ### Internal Fetch Calls If your Express routes make internal fetch calls to other services, you must manually propagate the test ID header:
```typescript
app.get("/api/dashboard", async (req, res) => {
// Extract test ID from incoming request
const testId = req.get(SCENARIST_TEST_ID_HEADER) || "default-test";
// Include test ID in internal fetch
const response = await fetch("http://localhost:3001/api/user", {
headers: {
[SCENARIST_TEST_ID_HEADER]: testId,
},
});
const data = await response.json();
res.json(data);
});
```
**Why this is needed:** * AsyncLocalStorage only tracks the current request * Internal fetch calls are new requests (separate context) * Must explicitly pass test ID header to maintain isolation ## What Makes Express Setup Special **Zero Boilerplate** - Scenarist uses `AsyncLocalStorage` to automatically track test IDs. No manual header passing required. **Test Isolation** - Each test gets its own scenario state. Tests run in parallel without interference. **Your Code Runs** - Your Express routes, middleware, validation, and business logic all execute normally. Only external API calls are mocked. ## Production Tree-Shaking Scenarist keeps its endpoints and request interception out of production through two layers: 1. **The `production` export condition** resolves `@scenarist/express-adapter` to a stub that returns `undefined` and imports nothing. No Scenarist or MSW code is loaded or bundled. 2. **A runtime guard**: when `NODE_ENV` is `production`, `createScenarist()` returns `undefined` even if the condition was not applied. No middleware, no `/__scenario__` endpoints, no interception. `createScenarist()` also returns `undefined` when `enabled` is `false`, so always guard with `if (scenarist)`. ### Unbundled Deployments (Most Common) `production` is a custom condition, so Node.js only applies it when you pass it explicitly:
```bash
# Recommended: resolves the zero-dependency stub
NODE_ENV=production node --conditions=production src/server.js
```
With the flag: * `createScenarist()` returns `undefined` at runtime * The production entry point imports nothing * MSW code **never loads into memory** If you start the server without the flag, the runtime guard still disables Scenarist:
```bash
# Scenarist is inert: createScenarist() returns undefined, /__scenario__ returns 404
NODE_ENV=production node src/server.js
```
Without --conditions=production, MSW must be installed Without the condition, Node.js loads the full adapter, which imports `msw`. If you install `msw` as a dev dependency and prune dev dependencies in production, the server fails at startup with `ERR_MODULE_NOT_FOUND`. Use `--conditions=production`, or keep `msw` installed. ### Bundled Deployments (esbuild, webpack, Vite, rollup) **For teams bundling Express server code**, configure the `production` condition. Without it, the runtime guard still disables Scenarist, but the adapter and MSW code stay in the bundle. Scenarist uses **conditional package.json exports** to provide different entry points:
```json
{
"exports": {
".": {
"production": "./dist/setup/production.js", // Zero dependencies
"default": "./dist/index.js" // Full implementation
}
}
}
```
The `"production"` condition is **custom** (not a Node.js built-in). Bundlers must be configured to recognize it: **esbuild:**
```json
{
"scripts": {
"build": "esbuild src/server.ts --bundle --conditions=production --define:process.env.NODE_ENV='\"production\"'"
}
}
```
**webpack:** webpack.config.js
```js
module.exports = {
mode: "production",
resolve: {
conditionNames: ["production", "import", "require"],
},
};
```
**Vite:** vite.config.js
```js
export default {
resolve: {
conditions: ["production"],
},
};
```
**rollup:**
```js
import resolve from "@rollup/plugin-node-resolve";
export default {
plugins: [
resolve({
exportConditions: ["production"],
}),
],
};
```
**Results:** * ✅ Bundle size: 618kb → 298kb (52% reduction) * ✅ Zero MSW code in production bundle **Verification:**
```bash
# Build with bundler configuration
npm run build:production
# Verify MSW code eliminated (succeeds only if dist/ exists and has no matches)
test -d dist && {
grep -rqE '(__scenarist_shared_msw_server|setupWorker|HttpResponse\.json)' dist/
test $? -eq 1
}
```
**For detailed configuration examples**, see the [Express Adapter README - Production Tree-Shaking](https://github.com/citypaul/scenarist/tree/main/packages/express-adapter#production-tree-shaking). ## Debugging with Logging If mocks aren’t matching as expected, enable logging to see what’s happening:
```typescript
import { createScenarist, createConsoleLogger } from "@scenarist/express-adapter";
const scenarist = createScenarist({
enabled: process.env.NODE_ENV === "test",
scenarios,
logger: createConsoleLogger({
level: "debug",
categories: ["matching", "scenario"],
}),
});
```
This shows which mocks are being evaluated and why they match or don’t. See [Logging Reference](/reference/logging/) for all options. ## Next Steps * **Debugging:** Learn about [logging options](/reference/logging/) for troubleshooting * **Production safety:** Learn why [Scenarist is safe for production](/concepts/production-safety/) * **Example app:** See [complete Express example](https://github.com/citypaul/scenarist/tree/main/apps/express-example) with comprehensive test suite * **Architecture:** Learn [how Scenarist works](/concepts/architecture/)