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
kyfor 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, orrunScriptexactly. - 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
asyncis 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 optionsRule: 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 datacreateXxx- Create objectsbuildXxx- 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 validhasXxx- Check for presencemeetsXxx- 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 conflictsDefine 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 fieldsTest 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
asyncunless 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/undefinedgracefully - Required parameters throw clear errors if missing
Error Handling:
- No try-catch (except custom assertions)
- Custom assertions use
$addAssertionResultproperly - 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
$secretsor$contextfor sensitive data - External data validated with Zod
- Uses API-first approach (no direct DB access)
Summary
Writing great ReAPI scripts is about:
- Leverage built-in tools - Use Zod for validation, lodash for data manipulation, faker for realistic data
- Be explicit - Clear names, good docs, obvious defaults
- Validate everything - Use Zod to ensure data quality
- Think about users - QA teams will use your functions, make them reliable
- Plan for change - Version functions, deprecate gracefully
- Test thoroughly - Edge cases matter
Following these practices will make your scripts robust, maintainable, and a joy to use.
Next Steps
Deepen your knowledge:
- Scripting Overview - Understand all script types
- Build a Testing Framework - See patterns in action
- Custom Assertions - Master assertion patterns
- Value Generators - Create powerful data generators
External Documentation:
- Zod Documentation - Complete Zod v4 reference
- Ky Documentation - Modern HTTP client
- Lodash Documentation - Utility functions
- Faker Documentation - Mock data generation