Scripting GuideAfter Response Hooks

After Response Hooks

After Response hooks run after an API request has completed and a response has been received. You can use these hooks to process the response, perform assertions, and update the test context with data from the response.

Multi-Level Hook System

After Response hooks in ReAPI follow the same 3-level hierarchy as Before Request hooks, but with reverse execution order to form a symmetric “onion model”:

  1. Node Level - Individual API response processing
  2. Folder Level - Module/feature-specific processing (with folder hierarchy)
  3. Test Flow Level - Global response handling for the entire test case

Each level supports two modes:

  • Inline: Write hook code directly at that level
  • Reference: Select a reusable hook function by ID (defined in Global Scripts)

Execution Order: Inner → Outer (REVERSED)

After Response hooks execute from inner to outer (specific to general), forming the symmetric counterpart to Before Request:

← API Response Received

Node Referenced Hooks (reusable functions)

Node Inline Hook (specific to this API)

Folder Hooks: Current → Parent → Root (module logic - REVERSED)

Test Flow Hooks (global cleanup/tracking)

Why this reverse order?

  • Start with endpoint-specific extraction (get data needed from this response)
  • Add module-level processing (decrypt, aggregate module data)
  • Finish with global cleanup/tracking (statistics, logging)
  • Like unwrapping layers of an onion from inside out

Symmetric Onion Pattern

Before Request (Build Up):              After Response (Unwrap):
┌─────────────────────────────────┐    ┌─────────────────────────────────┐
│ Test Flow: Auth, Debug          │    │ Test Flow: Cleanup, Stats       │
│  ┌───────────────────────────┐  │    │  ┌───────────────────────────┐  │
│  │ Folder: Module Headers    │  │    │  │ Folder: Decrypt, Aggregate│  │
│  │  ┌─────────────────────┐  │  │    │  │  ┌─────────────────────┐  │  │
│  │  │ Node: Specific Params│  │  │    │  │  │ Node: Extract Data  │  │  │
│  │  │    → API Call →      │  │  │    │  │  │  ← API Response ←   │  │  │
│  │  └─────────────────────┘  │  │    │  │  └─────────────────────┘  │  │
│  └───────────────────────────┘  │    │  └───────────────────────────┘  │
└─────────────────────────────────┘    └─────────────────────────────────┘

Execution Order Example

// Node Inline Hook: Extract specific data first
async function afterResponse() {
  $context.refundId = $response.data.refundId;
  $context.transactionId = $response.data.transactionId;
}
 
// Folder Hook (Payment Module): Decrypt and aggregate
async function afterResponse() {
  if ($response.data.encryptedCard) {
    $response.data.decryptedCard = $$PaymentUtils.decrypt(
      $response.data.encryptedCard
    );
  }
 
  $context.paymentStats = $context.paymentStats || { total: 0, approved: 0 };
  $context.paymentStats.total += 1;
  if ($response.data.status === "approved") {
    $context.paymentStats.approved += 1;
  }
}
 
// Test Flow Hook: Track global statistics
async function afterResponse() {
  $context.apiCallLog = $context.apiCallLog || [];
  $context.apiCallLog.push({
    url: $request.url,
    status: $response.status,
    time: $response.time,
  });
}

Result: Data extracted → Module processed → Global tracked—perfect unwrapping from inner to outer.


Script Pattern

An After Response hook must define an async function named afterResponse. This function has access to $request (read-only), $response, and $context.

async function afterResponse() {
  // Access response data
  const statusCode = $response.status;
  const responseData = $response.data;
 
  // Modify response data before it's passed to subsequent steps
  $response.data.processedAt = new Date().toISOString();
 
  // Update context with response information
  $context.lastResponseStatus = $response.status;
  if (responseData.token) {
    $context.token = responseData.token;
  }
 
  // Add assertions using Chai
  $expect($response.status).to.equal(200);
  $assert.isObject($response.data);
}

Key Points

  • Function Name: Must be afterResponse.
  • Async: Must be an async function.
  • Response Access: Use $response to read and modify the HTTP response.
  • Request Access: Use $request for read-only access to the original request.
  • Context Access: Use $context to read and write to the test context.
  • Assertions: Use the built-in $assert and $expect (Chai) to validate the response.

Available Response Properties

  • $response.status: HTTP status code
  • $response.data: Response body
  • $response.headers: Response headers
  • $response.statusText: HTTP status text

Example: Response Validation and Data Extraction

async function afterResponse() {
  // Validate response structure
  $expect($response.status).to.be.oneOf([200, 201]);
  $assert.isObject($response.data, "Response should be an object");
 
  // Extract data for subsequent requests
  if ($response.data.sessionToken) {
    $context.sessionToken = $response.data.sessionToken;
  }
 
  // Track metrics
  $context.lastApiCall = {
    url: $request.url,
    status: $response.status,
    duration: Date.now() - $context.requestStartTime,
  };
}

When to Use Each Level

LevelBest ForExamples
NodeEndpoint-specific data extractionExtract specific IDs, decrypt endpoint data, endpoint-specific assertions
FolderModule/feature-specific processingDecrypt module data, aggregate stats, module validation
Test FlowGlobal tracking and cleanupGlobal statistics, request logging, cleanup tasks

Why Reverse Order Matters

The reverse execution order (inner → outer) allows each layer to access and enhance what inner layers produced:

// Node Hook: Extract raw data
async function afterResponse() {
  $context.orderId = $response.data.orderId;
  $context.rawOrderData = $response.data;
}
 
// Folder Hook: Process that extracted data
async function afterResponse() {
  // Can access $context.orderId set by node hook
  const order = $context.rawOrderData;
  $context.processedOrder = $$OrderUtils.transformOrder(order);
}
 
// Test Flow Hook: Use processed data for global tracking
async function afterResponse() {
  // Can access $context.processedOrder set by folder hook
  $context.allOrders = $context.allOrders || [];
  $context.allOrders.push($context.processedOrder);
}

Reusable Hooks vs Inline Hooks

Define once in Global Scripts:

// Global Script
async function trackResponseMetrics() {
  $context.apiCallLog = $context.apiCallLog || [];
  $context.apiCallLog.push({
    url: $request.url,
    status: $response.status,
    time: $response.time,
    timestamp: Date.now(),
  });
}
 
async function extractCommonData() {
  if ($response.data.token) {
    $context.token = $response.data.token;
  }
  if ($response.data.userId) {
    $context.userId = $response.data.userId;
  }
}

Reference by ID in UI: QA selects “trackResponseMetrics” from dropdown at Test Flow level—applies to all API responses.

Benefits:

  • ✅ Write once, use everywhere
  • ✅ Consistent data extraction across all tests
  • ✅ QA can apply via UI without coding

Inline Hooks (For Specific Use Cases)

Write directly at Node level:

// Inline at Node level for a specific endpoint response
async function afterResponse() {
  // This specific endpoint returns encrypted audit trail
  if ($response.data.encryptedAuditTrail) {
    $response.data.auditTrail = $$PaymentUtils.decryptAuditTrail(
      $response.data.encryptedAuditTrail
    );
  }
}

Benefits:

  • ✅ Quick one-off response processing
  • ✅ Endpoint-specific logic stays with the endpoint
  • ✅ No need to create global function

Best Practices

1. Use Node Hooks for Specific Data Extraction

// ✅ Good: Extract exactly what this endpoint returns
async function afterResponse() {
  $context.paymentId = $response.data.paymentId;
  $context.transactionStatus = $response.data.status;
  $context.receiptUrl = $response.data.receiptUrl;
}

2. Use Folder Hooks for Module-Level Processing

// ✅ Good: Decrypt and aggregate for payment module
async function afterResponse() {
  // Decrypt sensitive payment data
  if ($response.data.encryptedCardLast4) {
    $response.data.cardLast4 = $$PaymentUtils.decrypt(
      $response.data.encryptedCardLast4
    );
  }
 
  // Aggregate payment statistics
  $context.paymentStats = $context.paymentStats || { total: 0, successful: 0 };
  $context.paymentStats.total += 1;
  if ($response.data.status === "approved") {
    $context.paymentStats.successful += 1;
  }
}

3. Use Test Flow Hooks for Global Tracking

// ✅ Good: Track all API calls across entire test
async function afterResponse() {
  $context.totalApiCalls = ($context.totalApiCalls || 0) + 1;
  $context.totalResponseTime =
    ($context.totalResponseTime || 0) + $response.time;
 
  $context.apiCallLog = $context.apiCallLog || [];
  $context.apiCallLog.push({
    url: $request.url,
    method: $request.method,
    status: $response.status,
    time: $response.time,
  });
}

4. Leverage Global Utilities for Complex Processing

// ✅ Good: Use global utilities for complex transformations
async function afterResponse() {
  // Validate response with Zod schema
  const parseResult = $$Schemas.apiResponse.safeParse($response.data);
  if (!parseResult.success) {
    $context.validationErrors = parseResult.error.errors;
    return;
  }
 
  // Transform timestamps to readable format
  $response.data.createdAtFormatted = $$DateUtils.formatTimestamp(
    $response.data.createdAt
  );
 
  // Generate data integrity hash
  $response.data.hash = $$DataUtils.generateHash($response.data);
}

5. Use Assertions Appropriately by Level

// ✅ Node Level: Endpoint-specific assertions
async function afterResponse() {
  $expect($response.status).to.equal(201);
  $expect($response.data.orderId).to.be.a("string");
}
 
// ✅ Folder Level: Module business rules
async function afterResponse() {
  // Payment module: ensure fraud check was performed
  $expect($response.data.fraudCheckStatus).to.exist;
  $expect($response.data.fraudScore).to.be.within(0, 100);
}
 
// ✅ Test Flow Level: Global assertions
async function afterResponse() {
  // All APIs should return within SLA
  $expect($response.time).to.be.lessThan($context.config.slaThreshold);
}

6. Transform Data Progressively

// Node Hook: Extract raw data
async function afterResponse() {
  $context.rawMetrics = $response.data.metrics;
}
 
// Folder Hook: Transform for module needs
async function afterResponse() {
  $context.processedMetrics = $$AnalyticsUtils.processMetrics(
    $context.rawMetrics
  );
}
 
// Test Flow Hook: Aggregate across all API calls
async function afterResponse() {
  $context.allMetrics = $context.allMetrics || [];
  $context.allMetrics.push(...$context.processedMetrics);
}

Key Takeaways

  1. Reverse Onion Pattern - Node → Folder → Test Flow (inner to outer)
  2. Symmetric with Before Request - Build up request, unwrap response
  3. Each Layer Enhances - Inner layers extract, outer layers aggregate
  4. Inline or Reference - Write directly or select from global functions
  5. Use Assertions at All Levels - Specific → Module → Global
  6. Progressive Transformation - Extract → Process → Aggregate
  7. Leverage Utilities - Call $$Utils for complex operations

For a comprehensive example of the multi-level hook system, see Build a Testing Framework.