Scripting GuideBest Practices

Script Writing Best Practices

These are battle-tested practices for writing maintainable, reliable scripts in ReAPI. Follow these guidelines to create scripts that are robust, reusable, and easy for your team to work with.


Built-in Runtime Libraries

Before diving into patterns, know what’s always available in your scripts:

Runtime Built-ins (No Loading Required):

  • lodash (_) - Data manipulation and utilities
  • faker - Realistic mock data generation
  • ky - Modern, lightweight HTTP client (recommended)
  • fetch - Native fetch API (MDN)
  • Zod (z) - Schema validation and type safety (v4.x)

These libraries are part of ReAPI’s runtime and available in all script types immediately.

Notes:

  • ReAPI includes Zod v4.x. If you’re familiar with Zod v3, be aware of the breaking changes in v4.
  • Use ky for most HTTP needs. For advanced features like interceptors or upload progress, you can load Axios from predefined libraries.

Function Basics

Rule: Always Write Named Functions (Except Global Scripts)

Each script type requires a specific function signature. Important: There are two types of function names with fundamentally different rules:

Type 1: Fixed Function Names (Cannot Change)

These hooks must use exact function names - ReAPI looks for these specific names:

Before Request Hook:

async function beforeRequest() {
  // MUST be named "beforeRequest" - this is a fixed name
  // Modify $request before sending
}

After Response Hook:

async function afterResponse() {
  // MUST be named "afterResponse" - this is a fixed name
  // Process $response after receiving
}

Script Node:

async function runScript() {
  // MUST be named "runScript" - this is a fixed name
  // Execute custom logic
}

Type 2: Custom Function Names (Must Match Your Title)

These functions must match the name you give when creating them in the UI:

Value Generator:

// If you create a generator with title "generateUser"
async function generateUser(options) {
  // Function name MUST exactly match the title
  return { id: faker.string.uuid() };
}
 
// If you create a generator with title "createTestOrder"
async function createTestOrder(options) {
  // Function name MUST exactly match the title
  return { orderId: faker.string.uuid() };
}

Custom Assertion:

// If you create an assertion with title "isValidUser"
async function isValidUser(value) {
  // Function name MUST exactly match the title
  $addAssertionResult({
    /* ... */
  });
}
 
// If you create an assertion with title "meetsBusinessRules"
async function meetsBusinessRules(value) {
  // Function name MUST exactly match the title
  $addAssertionResult({
    /* ... */
  });
}

Key Difference:

  • Hooks and Script Node: Fixed names defined by ReAPI. Use beforeRequest, afterResponse, or runScript exactly.
  • Generators and Assertions: Variable names that you define. The function name must match whatever title you entered in the UI when creating it.

Exception: Global scripts define classes and utility functions that are called by other scripts.

Rule: Use Async Functions (Almost Always)

Default to async for all functions:

Recommended:

async function generateUser(options) {
  // Even if not async today, you might need to fetch data tomorrow
  return { id: faker.string.uuid() };
}

Only exceptions:

  • Value generators without async operations (but async is safer)
  • Simple custom assertions without API calls

Why: Starting with async prevents breaking changes when you need to add async operations later.


Parameter Design and Validation

Rule: Value Generators Accept Zero or One Parameter

No Parameters:

function getCurrentTimestamp() {
  return Date.now();
}

Simple Parameter:

function generateUsers(count) {
  return Array.from({ length: count }, (_, i) => ({
    id: i + 1,
    name: faker.person.fullName(),
  }));
}

Complex Parameter (Recommended):

function generateUser(options) {
  // Always handle null/undefined
  const { role = "user", includeAddress = false } = options || {};
 
  return {
    id: faker.string.uuid(),
    email: faker.internet.email(),
    role: role,
    ...(includeAddress && { address: faker.location.streetAddress() }),
  };
}

Use Zod for Parameter Validation

For complex parameters, use Zod to validate and provide type safety:

Best Practice:

async function generateUser(options) {
  // Define schema with validation and defaults
  const schema = z.object({
    role: z.enum(["free", "pro", "enterprise"]).default("free"),
    includeAddress: z.boolean().default(false),
    count: z.number().min(1).max(100).optional(),
  });
 
  // Parse and validate - throws clear error if invalid
  const params = schema.parse(options);
 
  return {
    id: faker.string.uuid(),
    email: faker.internet.email(),
    role: params.role,
    ...(params.includeAddress && {
      address: faker.location.streetAddress(),
    }),
  };
}

Benefits:

  • Automatic validation with clear error messages
  • Built-in default values
  • Type constraints (enums, min/max, patterns)
  • Self-documenting code

Validate Required Parameters

For required parameters, use Zod or throw early:

function fetchUserData(userId) {
  const schema = z.object({
    userId: z.string().min(1, "userId is required"),
  });
 
  const { userId: validId } = schema.parse({ userId });
 
  // Now safe to use validId
  return ky.get(`/users/${validId}`).json();
}

Or throw directly for simple cases:

function fetchUserData(userId) {
  if (!userId) {
    throw new Error("userId is required");
  }
  return ky.get(`/users/${userId}`).json();
}

Error Handling

Rule: Do NOT Use Try-Catch (Except Assertions)

Let errors bubble up so the UI can display them properly.

Wrong:

async function generateUser() {
  try {
    return { id: faker.string.uuid() };
  } catch (error) {
    return null; // UI won't show the error!
  }
}

Right:

async function generateUser() {
  // Let errors bubble up - UI will display them
  return { id: faker.string.uuid() };
}

Exception: Custom Assertions Must Catch Errors

Assertions MUST use try-catch and report via $addAssertionResult:

async function isValidUser(user) {
  try {
    // Validate using Zod
    const userSchema = z.object({
      id: z.string().uuid(),
      email: z.string().email(),
      role: z.enum(["user", "admin", "guest"]),
    });
 
    const result = userSchema.safeParse(user);
 
    $addAssertionResult({
      passed: result.success,
      message: result.success
        ? "User is valid"
        : result.error.issues.map((i) => i.message).join(", "),
      operator: "isValidUser",
      leftValue: user,
      rightValue: "valid user schema",
    });
  } catch (error) {
    $addAssertionResult({
      passed: false,
      message: `Error: ${error.message}`,
      operator: "isValidUser",
      leftValue: user,
      rightValue: "valid user object",
    });
  }
}

Note: Zod’s safeParse() doesn’t throw errors, making it perfect for assertions.


Custom Assertions with Zod

Zod makes custom assertions powerful and maintainable:

Pattern: Schema-Based Assertions

async function isValidOrder(order) {
  try {
    const orderSchema = z.object({
      orderId: z.string().uuid(),
      customerId: z.string().min(1),
      total: z.number().positive(),
      status: z.enum(["pending", "processing", "completed", "cancelled"]),
      items: z
        .array(
          z.object({
            productId: z.string(),
            quantity: z.number().int().positive(),
            price: z.number().positive(),
          })
        )
        .min(1, "Order must have at least one item"),
      createdAt: z.string().datetime(),
    });
 
    const result = orderSchema.safeParse(order);
 
    $addAssertionResult({
      passed: result.success,
      message: result.success
        ? `Order is valid with ${order.items?.length} items`
        : `Validation failed: ${result.error.issues
            .map((i) => `${i.path.join(".")}: ${i.message}`)
            .join(", ")}`,
      operator: "isValidOrder",
      leftValue: order,
      rightValue: "valid order schema",
    });
  } catch (error) {
    $addAssertionResult({
      passed: false,
      message: `Error: ${error.message}`,
      operator: "isValidOrder",
      leftValue: order,
      rightValue: "valid order",
    });
  }
}

Reusable Schemas in Global Scripts

Define common schemas in global scripts:

class $$Schemas {
  static get user() {
    return z.object({
      id: z.string().uuid(),
      email: z.string().email(),
      name: z.string().min(1),
      role: z.enum(["free", "pro", "enterprise"]),
    });
  }
 
  static get order() {
    return z.object({
      orderId: z.string().uuid(),
      customerId: z.string(),
      total: z.number().positive(),
      status: z.enum(["pending", "completed", "cancelled"]),
    });
  }
}

Then use in assertions:

async function isValidUser(user) {
  const result = $$Schemas.user.safeParse(user);
  // ... report result
}

Function Lifecycle Management

Rule: Test Thoroughly Before Activating

Use ReAPI’s built-in test UI to validate:

  • ✓ Different parameter combinations
  • ✓ Edge cases (null, undefined, empty strings/objects)
  • ✓ Invalid data types
  • ✓ Error scenarios
  • ✓ Performance with large data

Example test cases for a generator:

// Test these before activating:
generateUser(); // No params
generateUser({ role: "pro" }); // Valid param
generateUser({ role: "invalid" }); // Should fail validation
generateUser(null); // Should handle gracefully
generateUser({ role: "enterprise", includeAddress: true }); // All options

Rule: Never Disable Activated Functions

Once a function is activated and used in test cases:

Don’t:

  • Disable it (dependent tests will fail)
  • Delete it
  • Change its signature without versioning

Do:

  • Mark as deprecated
  • Create v2 with new behavior
  • Keep old version working

Exception: Functions not yet deployed to production can be modified freely.


Versioning and Compatibility

Rule: Version Functions for Breaking Changes

When you need to change behavior incompatibly, create a new version:

Option 1 - Version Number:

// Old version - keep working
function generateUser() {
  return { id: faker.string.uuid(), name: faker.person.fullName() };
}
 
// New version with enhanced features
function generateUserV2(options) {
  const schema = z.object({
    role: z.enum(["free", "pro", "enterprise"]).default("free"),
    includeAddress: z.boolean().default(false),
  });
 
  const params = schema.parse(options);
  // ... generate with new logic
}

Option 2 - Backward Compatible:

function generateUser(options) {
  // Handle legacy single number parameter
  if (typeof options === "number") {
    return Array.from({ length: options }, () => ({
      id: faker.string.uuid(),
      name: faker.person.fullName(),
    }));
  }
 
  // Handle new object parameter
  const schema = z
    .object({
      count: z.number().default(1),
      role: z.enum(["free", "pro", "enterprise"]).default("free"),
    })
    .optional();
 
  const params = schema.parse(options);
  // ... generate with params
}

Deprecation Pattern

/**
 * @deprecated Use generateUserV2 instead. Will be removed in Q2 2024.
 * This function is maintained for backward compatibility only.
 */
function generateUser() {
  // Forward to new version with default params
  return generateUserV2({ role: "user" });
}

Naming Conventions

Function Names

Remember: Only Value Generators and Custom Assertions have custom names. Hooks and Script Nodes use fixed names (beforeRequest, afterResponse, runScript).

Value Generators:

These are your custom names - choose descriptive names following these patterns:

  • generateXxx - Generate new data
  • createXxx - Create objects
  • buildXxx - Build complex structures

Examples: generateUser, createTestOrder, buildMetricsData

Important: The function name must exactly match the title you enter in the UI when creating the generator.

Custom Assertions:

These are your custom names - choose names that describe what is being validated:

  • isXxx - Check if something is valid
  • hasXxx - Check for presence
  • meetsXxx - Check if meets criteria

Examples: isValidUser, hasRequiredFields, meetsBusinessRules

Important: The function name must exactly match the title you enter in the UI when creating the assertion.

Utility Functions (in Global Scripts):

  • Use descriptive verbs
  • Be specific about what they do

Examples: formatTimestamp, calculateGrowth, parseApiResponse

Global Classes

  • Use $$ prefix: $$DateUtils, $$AuthHelpers, $$Schemas
  • Class names in PascalCase
  • Static methods only
class $$ValidationHelpers {
  static isEmail(str) {
    return z.string().email().safeParse(str).success;
  }
 
  static isUUID(str) {
    return z.string().uuid().safeParse(str).success;
  }
}

Code Quality

Always Provide Defaults

function generateMetrics(options) {
  const schema = z.object({
    type: z.enum(["daily", "weekly", "monthly"]).default("daily"),
    count: z.number().min(1).max(1000).default(10),
    includeHistory: z.boolean().default(false),
  });
 
  const params = schema.parse(options);
  // ... use params with guaranteed defaults
}

Use Descriptive Variable Names

Bad:

const d = new Date();
const u = response.data;
const x = calculateValue(a, b);

Good:

const currentTimestamp = new Date();
const userData = response.data;
const growthPercentage = calculateGrowth(currentValue, previousValue);

Add JSDoc Comments

/**
 * Generate test user with specified role and features
 * @param {Object} options - Configuration options
 * @param {'free'|'pro'|'enterprise'} options.role - User subscription tier
 * @param {boolean} options.includeAddress - Include address fields
 * @param {number} options.orderCount - Number of past orders to generate
 * @returns {Object} Generated user object with all specified fields
 * @throws {ZodError} If options don't match schema
 */
async function generateUser(options) {
  const schema = z.object({
    role: z.enum(["free", "pro", "enterprise"]).default("free"),
    includeAddress: z.boolean().default(false),
    orderCount: z.number().min(0).max(100).default(0),
  });
 
  const params = schema.parse(options);
  // ... implementation
}

Performance Guidelines

Avoid Large Loops

Avoid:

for (let i = 0; i < 10000; i++) {
  // Expensive operation
}

Limit iterations:

const MAX_ITEMS = 100;
const safeCount = Math.min(requestedCount, MAX_ITEMS);
 
for (let i = 0; i < safeCount; i++) {
  // Safe operation
}

Set Timeouts for HTTP Requests

async function fetchTestData(url) {
  const data = await ky
    .get(url, {
      timeout: 5000, // 5 second timeout
    })
    .json();
  return data;
}

Don’t Generate Huge Data

async function generateUsers(options) {
  const schema = z.object({
    count: z.number().min(1).max(1000).default(10), // Hard limit in schema
  });
 
  const { count } = schema.parse(options);
 
  return Array.from({ length: count }, () => ({
    id: faker.string.uuid(),
    name: faker.person.fullName(),
    email: faker.internet.email(),
  }));
}

Why: The Zod schema enforces the maximum, preventing accidental generation of millions of records.


Security Best Practices

Never Hardcode Secrets

Never:

const apiKey = "sk_live_1234567890";
const dbPassword = "password123";

Use secrets or context:

const apiKey = $secrets.apiKey || $context.apiKey;
const authToken = $context.accessToken;

Use APIs, Not Direct DB Access

Don’t:

const dbConnection = connectToDatabase(dbUrl);
const users = await db.query("SELECT * FROM users");

Do:

const users = await ky
  .get("https://test-data-api.com/users", {
    headers: { Authorization: `Bearer ${$secrets.apiToken}` },
  })
  .json();

Why: API-first approach is more secure, maintainable, and follows microservices patterns.

Validate External Data with Zod

async function fetchTestData(url) {
  const response = await ky.get(url).json();
 
  // Define expected schema
  const schema = z.object({
    data: z.array(
      z.object({
        id: z.string(),
        name: z.string(),
      })
    ),
  });
 
  // Validate response structure
  const validated = schema.parse(response);
 
  return validated.data;
}

Global Script Specific

Use Class-Based Structure

Good - No global pollution:

class $$Utils {
  static formatDate(date) {
    return dayjs(date).format("YYYY-MM-DD");
  }
 
  static validateEmail(email) {
    return z.string().email().safeParse(email).success;
  }
}

Avoid - Pollutes global scope:

function formatDate(date) {
  return dayjs(date).format("YYYY-MM-DD");
}
 
// This function is now in global scope, potential conflicts

Define Reusable Schemas

class $$Schemas {
  static get user() {
    return z.object({
      id: z.string().uuid(),
      email: z.string().email(),
      name: z.string().min(1),
      role: z.enum(["free", "pro", "enterprise"]),
      createdAt: z.string().datetime(),
    });
  }
 
  static get apiResponse() {
    return z.object({
      status: z.number().int().min(100).max(599),
      data: z.unknown(),
      timestamp: z.number(),
    });
  }
 
  static metric(type) {
    return z.object({
      value: z.number(),
      change: z.number(),
      timestamp: z.number(),
      type: z.literal(type),
    });
  }
}

Usage:

// In custom assertion
const result = $$Schemas.user.safeParse(userData);
 
// In value generator
const params = $$Schemas.metric("user_growth").parse(options);

Document Public APIs

/**
 * Global utilities for data validation
 * Available in all scripts as $$ValidationUtils
 */
class $$ValidationUtils {
  /**
   * Validate if value is a valid UUID
   * @param {any} value - Value to validate
   * @returns {boolean} True if valid UUID
   */
  static isUUID(value) {
    return z.string().uuid().safeParse(value).success;
  }
 
  /**
   * Validate email address
   * @param {any} value - Value to validate
   * @returns {boolean} True if valid email
   */
  static isEmail(value) {
    return z.string().email().safeParse(value).success;
  }
}

Testing Best Practices

Test with Edge Cases

Before activating any function, test:

  • ✓ Null/undefined parameters
  • ✓ Empty strings, arrays, objects
  • ✓ Very large numbers
  • ✓ Invalid data types
  • ✓ Network failures (for HTTP calls)
  • ✓ Validation errors (for Zod schemas)

Example test suite for a generator:

// Normal usage
generateUser({ role: "pro" });
 
// Edge cases
generateUser(); // No params - should use defaults
generateUser(null); // Null - should handle gracefully
generateUser({}); // Empty object - should use defaults
generateUser({ role: "invalid_role" }); // Should throw validation error
generateUser({ role: "enterprise", unknownField: true }); // Extra fields

Test Zod Validation Errors

Make sure your Zod schemas fail appropriately:

// Test that validation rejects invalid data
try {
  generateUser({ role: "superadmin" }); // Not in enum
  console.log("ERROR: Should have thrown validation error");
} catch (error) {
  console.log("PASS: Validation rejected invalid role");
}

Common Pitfalls to Avoid

❌ Modifying $context in Value Generators

Value generators should be pure functions:

// ❌ Don't - Side effects
function generateUser() {
  $context.lastUser = { id: faker.string.uuid() }; // Bad!
  return $context.lastUser;
}
 
// ✅ Do - Pure function
function generateUser() {
  return { id: faker.string.uuid() }; // Clean
}

❌ Returning Inconsistent Types

// ❌ Bad - Sometimes object, sometimes null
function getUser(id) {
  if (id) return { id, name: "John" };
  return null;
}
 
// ✅ Good - Always returns object
function getUser(id) {
  return { id: id || "unknown", name: "John" };
}

❌ Forgetting to Handle Null/Undefined Options

// ❌ Will crash if called without params
function generate(options) {
  const { type } = options; // Error if options is undefined
}
 
// ✅ Safe with Zod
function generate(options) {
  const schema = z
    .object({
      type: z.string().default("default"),
    })
    .optional();
 
  const params = schema.parse(options);
}
 
// ✅ Safe with manual handling
function generate(options) {
  const { type = "default" } = options || {};
}

❌ Not Using Zod’s Full Power

// ❌ Manual validation - verbose and error-prone
function generateMetrics(options) {
  if (!options) options = {};
  if (typeof options.count !== "number") options.count = 10;
  if (options.count < 1 || options.count > 100) {
    throw new Error("count must be between 1 and 100");
  }
  if (!["daily", "weekly", "monthly"].includes(options.type)) {
    options.type = "daily";
  }
  // ... lots more validation
}
 
// ✅ Zod - clean, declarative, type-safe
function generateMetrics(options) {
  const schema = z.object({
    count: z.number().min(1).max(100).default(10),
    type: z.enum(["daily", "weekly", "monthly"]).default("daily"),
  });
 
  const params = schema.parse(options);
  // Validation done, params are guaranteed valid
}

Quick Reference Checklist

Before activating a function:

Function Structure:

  • Function has correct name and signature for its type
  • Uses async unless specifically sync-only
  • Has JSDoc comments explaining purpose and parameters

Parameters:

  • Parameters validated with Zod schema (for complex params)
  • All parameters have default values
  • Handles null/undefined gracefully
  • Required parameters throw clear errors if missing

Error Handling:

  • No try-catch (except custom assertions)
  • Custom assertions use $addAssertionResult properly
  • Validation errors are descriptive

Testing:

  • Tested with normal parameters
  • Tested with null/undefined
  • Tested with invalid data (Zod validation)
  • Tested edge cases (empty, large numbers, etc.)
  • Performance tested with realistic data sizes

Code Quality:

  • Descriptive variable names
  • No hardcoded secrets or URLs
  • No side effects (except hooks)
  • Returns consistent types
  • Performance limits in place

Security:

  • Uses $secrets or $context for sensitive data
  • External data validated with Zod
  • Uses API-first approach (no direct DB access)

Summary

Writing great ReAPI scripts is about:

  1. Leverage built-in tools - Use Zod for validation, lodash for data manipulation, faker for realistic data
  2. Be explicit - Clear names, good docs, obvious defaults
  3. Validate everything - Use Zod to ensure data quality
  4. Think about users - QA teams will use your functions, make them reliable
  5. Plan for change - Version functions, deprecate gracefully
  6. Test thoroughly - Edge cases matter

Following these practices will make your scripts robust, maintainable, and a joy to use.


Next Steps

Deepen your knowledge:

External Documentation: