Skip to content

A lightweight TypeScript library for mocking API endpoints during frontend development. Perfect for developing frontend applications before the backend is ready, or for testing different API scenarios.

License

Notifications You must be signed in to change notification settings

onamfc/api-mocker

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

API Mocker

A lightweight TypeScript library for mocking API endpoints during frontend development. Perfect for developing frontend applications before the backend is ready, or for testing different API scenarios.

Features

  • Easy Integration: Simple API that works with any HTTP client (fetch, axios, etc.)
  • TypeScript Support: Full TypeScript support with type definitions
  • Flexible Matching: Support for exact paths and dynamic parameters (:id)
  • Advanced Matching: Match requests by query parameters, headers, or custom logic
  • Dynamic Responses: Create responses with custom logic
  • Stateful Mocks: Built-in state management for realistic CRUD operations
  • Network Error Simulation: Simulate timeouts, connection errors, and aborts
  • Priority System: Control which mock takes precedence with priority levels
  • Delay Simulation: Simulate network delays for realistic testing
  • Request Logging: Optional request logging for debugging
  • Easy Switching: Simple enable/disable for switching to real APIs
  • No Dependencies: Zero runtime dependencies

Requirements

  • Node.js 18 or later for local development and testing (earlier versions must polyfill the Fetch API).
  • Browsers or runtimes that expose fetch, Request, and Response globals. When targeting older environments, install a fetch polyfill before creating an ApiMocker instance.

Installation

npm install @onamfc/api-mocker

Quick Start Guide

Step 1: Create Your Mock Setup File

Create a dedicated file for your API mocks. This keeps your mock configuration organized and separate from your main application code.

Create src/mocks/api-mocks.ts

import { createMocker } from '@onamfc/api-mocker';

// Create the mocker instance
export const apiMocker = createMocker({
  baseUrl: 'https://api.yourapp.com', // Your API base URL
  logRequests: true, // Enable logging to see which requests are being mocked
  globalDelay: 500   // Add 500ms delay to all requests for realism
});

// Define your mock endpoints
export function setupMocks() {
  // Simple GET endpoint
  apiMocker.get('/users', [
    { id: 1, name: 'John Doe', email: 'john@example.com', role: 'admin' },
    { id: 2, name: 'Jane Smith', email: 'jane@example.com', role: 'user' },
    { id: 3, name: 'Bob Johnson', email: 'bob@example.com', role: 'user' }
  ]);

  // Dynamic endpoint with parameters
  apiMocker.get('/users/:id', {
    id: 1,
    name: 'John Doe',
    email: 'john@example.com',
    role: 'admin',
    createdAt: '2023-01-15T10:30:00Z',
    lastLogin: '2024-01-20T14:22:00Z'
  });

  // POST endpoint with custom status
  apiMocker.post('/users', {
    id: 4,
    name: 'New User',
    email: 'newuser@example.com',
    role: 'user',
    createdAt: new Date().toISOString()
  }, {
    status: 201,
    delay: 1000 // Longer delay for POST requests
  });

  // PUT endpoint
  apiMocker.put('/users/:id', {
    id: 1,
    name: 'Updated User',
    email: 'updated@example.com',
    role: 'admin',
    updatedAt: new Date().toISOString()
  });

  // DELETE endpoint
  apiMocker.delete('/users/:id', {
    success: true,
    message: 'User deleted successfully'
  });
}

Step 2: Initialize Mocks in Your Application

For React Applications:

đź’ˇ Environment variable tips: Create React App (and other Webpack-based builds) exposes values through process.env, while Vite exposes them via import.meta.env. Use the snippet that matches your toolchain so the browser build never tries to access an undefined process object.

Create src/mocks/index.ts

For Create React App / Webpack projects:

import { setupMocks, apiMocker } from './api-mocks';

const env = typeof process !== 'undefined' && process?.env ? process.env : {};

// Only enable mocks in development
const isDevelopment = env.NODE_ENV === 'development';
const useMocks = env.REACT_APP_USE_MOCKS === 'true' || isDevelopment;

if (useMocks) {
  console.log('API Mocking enabled');
  setupMocks();
} else {
  console.log('Using real API');
  apiMocker.disable();
}

export { apiMocker };

Or for Vite-powered React projects:

import { setupMocks, apiMocker } from './api-mocks';

const env = import.meta.env;

// Only enable mocks in development
const isDevelopment = env.MODE === 'development' || env.DEV;
const useMocks = env.VITE_USE_MOCKS === 'true' || isDevelopment;

if (useMocks) {
  console.log('API Mocking enabled');
  setupMocks();
} else {
  console.log('Using real API');
  apiMocker.disable();
}

export { apiMocker };

Then in your src/index.js or src/main.tsx:

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

// Import mocks BEFORE your app components
import './mocks';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

For Vue Applications:

Create src/mocks/index.ts:

import { setupMocks, apiMocker } from './api-mocks';

export function initializeMocks() {
  const isDevelopment = import.meta.env.DEV;
  const useMocks = import.meta.env.VITE_USE_MOCKS === 'true' || isDevelopment;

  if (useMocks) {
    console.log('API Mocking enabled');
    setupMocks();
  } else {
    console.log('Using real API');
    apiMocker.disable();
  }
}

Then in your src/main.js:

import { createApp } from 'vue';
import App from './App.vue';
import { initializeMocks } from './mocks';

// Initialize mocks before creating the app
initializeMocks();

createApp(App).mount('#app');

Step 3: Use Your Regular HTTP Client

Now your existing API calls will automatically use the mocked responses:

// This will use your mocked data automatically
async function fetchUsers() {
  try {
    const response = await fetch('https://api.yourapp.com/users');
    const users = await response.json();
    console.log('Users:', users); // Will log your mocked users
    return users;
  } catch (error) {
    console.error('Failed to fetch users:', error);
  }
}

// This will also work with dynamic parameters
async function fetchUser(id) {
  const response = await fetch(`https://api.yourapp.com/users/${id}`);
  return response.json();
}

// POST requests work too
async function createUser(userData) {
  const response = await fetch('https://api.yourapp.com/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(userData)
  });
  return response.json();
}

API Reference

createMocker(config?: MockConfig)

Factory helper that returns a new ApiMocker instance. Use this in your application bootstrap so you can easily share a configured mocker across modules.

ApiMocker

The main class responsible for intercepting fetch calls.

  • mock(endpoint) – Register a static endpoint. The response can be plain data, a MockResponse object ({ data, status?, headers? }), a native Response, or a function returning any of those (sync or async).
  • mockDynamic(endpoint) – Register a dynamic endpoint whose handler receives a RequestContext with params, query, headers, parsed body, and shared state.
  • get, post, put, patch, delete – Convenience wrappers around mock for common HTTP verbs.
  • enable() / disable() – Toggle interception without removing registered mocks.
  • clear() / remove(method, path) – Remove all mocks or a specific endpoint.
  • setState(key, value) / getState() / clearState() – Manage shared in-memory state used by dynamic handlers.
  • updateConfig(partialConfig) – Merge configuration updates at runtime (e.g., change baseUrl, add headers, adjust logging).
  • restore() – Restore the original fetch implementation. Call this when you no longer need mocking (for example in test teardown).

Endpoint Options

  • status, delay, headers, priority – Control the returned HTTP status, simulated latency, custom headers, and selection priority between overlapping mocks.
  • queryParams, requestHeaders, match(request) – Advanced matching rules to scope mocks to specific query strings, request headers, or arbitrary logic.
  • networkError – Simulate network failures ('timeout', 'connection_refused', or 'abort').
  • MockResponse support lets you fine-tune status and headers on a per-response basis without mutating the endpoint definition.

Advanced Usage Examples

1. Dynamic Responses with Custom Logic

// Create a more realistic user endpoint that responds based on the ID
apiMocker.mockDynamic({
  path: '/users/:id',
  method: 'GET',
  handler: (context) => {
    const userId = parseInt(context.params.id);
    
    // Simulate user not found
    if (userId > 100) {
      throw new Error('User not found');
    }
    
    return {
      id: userId,
      name: `User ${userId}`,
      email: `user${userId}@example.com`,
      role: userId === 1 ? 'admin' : 'user',
      avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${userId}`,
      createdAt: new Date(2023, 0, userId).toISOString(),
      isOnline: Math.random() > 0.5
    };
  }
});

2. Stateful CRUD Operations

Create src/mocks/stateful-mocks.ts:

import { apiMocker } from './api-mocks';

let nextId = 4;

// Initialize some data
apiMocker.setState('users', [
  { id: 1, name: 'John Doe', email: 'john@example.com' },
  { id: 2, name: 'Jane Smith', email: 'jane@example.com' },
  { id: 3, name: 'Bob Johnson', email: 'bob@example.com' }
]);

export function setupStatefulMocks() {
  // GET all users - returns current state
  apiMocker.mockDynamic({
    path: '/users',
    method: 'GET',
    handler: (context) => {
      return context.state.users || [];
    }
  });

  // GET single user
  apiMocker.mockDynamic({
    path: '/users/:id',
    method: 'GET',
    handler: (context) => {
      const users = context.state.users || [];
      const user = users.find(u => u.id === parseInt(context.params.id));
      
      if (!user) {
        return { error: 'User not found' };
      }
      
      return user;
    },
    status: 200
  });

  // POST new user - adds to state
  apiMocker.mockDynamic({
    path: '/users',
    method: 'POST',
    handler: (context) => {
      const users = context.state.users || [];
      const newUser = {
        id: nextId++,
        ...context.body,
        createdAt: new Date().toISOString()
      };
      
      users.push(newUser);
      context.state.users = users;
      
      return newUser;
    },
    status: 201
  });

  // PUT update user - modifies state
  apiMocker.mockDynamic({
    path: '/users/:id',
    method: 'PUT',
    handler: (context) => {
      const users = context.state.users || [];
      const userIndex = users.findIndex(u => u.id === parseInt(context.params.id));
      
      if (userIndex === -1) {
        return { error: 'User not found' };
      }
      
      users[userIndex] = {
        ...users[userIndex],
        ...context.body,
        updatedAt: new Date().toISOString()
      };
      
      context.state.users = users;
      return users[userIndex];
    }
  });

  // DELETE user - removes from state
  apiMocker.mockDynamic({
    path: '/users/:id',
    method: 'DELETE',
    handler: (context) => {
      const users = context.state.users || [];
      const userIndex = users.findIndex(u => u.id === parseInt(context.params.id));
      
      if (userIndex === -1) {
        return { error: 'User not found' };
      }
      
      users.splice(userIndex, 1);
      context.state.users = users;
      
      return { success: true, message: 'User deleted' };
    }
  });
}

3. Advanced Request Matching

// Match based on query parameters
apiMocker.mock({
  path: '/users',
  method: 'GET',
  response: [{ id: 1, name: 'Admin User', role: 'admin' }],
  queryParams: { role: 'admin' }
});

apiMocker.mock({
  path: '/users',
  method: 'GET',
  response: [
    { id: 2, name: 'Regular User 1', role: 'user' },
    { id: 3, name: 'Regular User 2', role: 'user' }
  ],
  queryParams: { role: 'user' }
});

// Match based on request headers
apiMocker.mock({
  path: '/api/data',
  method: 'GET',
  response: { format: 'json', data: [1, 2, 3] },
  requestHeaders: { 'Accept': 'application/json' }
});

// Custom matching logic
apiMocker.mock({
  path: '/protected',
  method: 'GET',
  response: { message: 'Access granted', data: 'secret' },
  match: (request) => {
    const authHeader = request.headers.get('Authorization');
    return authHeader && authHeader.startsWith('Bearer ');
  }
});

4. Error Simulation

// HTTP error responses
apiMocker.get('/users/999', { error: 'User not found' }, {
  status: 404,
  delay: 300
});

apiMocker.post('/users', { error: 'Validation failed', details: ['Email is required'] }, {
  status: 400
});

// Network error simulation
apiMocker.mock({
  path: '/flaky-endpoint',
  method: 'GET',
  response: {},
  networkError: 'timeout' // Simulates network timeout
});

// Conditional errors
apiMocker.mockDynamic({
  path: '/unreliable',
  method: 'GET',
  handler: () => {
    // 30% chance of success, 70% chance of error
    if (Math.random() > 0.3) {
      throw new Error('Service temporarily unavailable');
    }
    return { status: 'success', data: 'Hello World' };
  }
});

File Organization

Here's a recommended file structure for organizing your mocks:

src/
├── mocks/
│   ├── index.ts              # Main mock initialization
│   ├── api-mocks.ts          # Basic mock definitions
│   ├── stateful-mocks.ts     # Stateful CRUD operations
│   ├── error-mocks.ts        # Error scenarios
│   └── data/
│       ├── users.ts          # Mock user data
│       ├── products.ts       # Mock product data
│       └── orders.ts         # Mock order data
├── services/
│   ├── api.ts                # Your API service functions
│   └── userService.ts        # User-specific API calls
└── components/
    └── ...

Example src/mocks/data/users.ts:

export const mockUsers = [
  {
    id: 1,
    name: 'John Doe',
    email: 'john@example.com',
    role: 'admin',
    avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=1',
    createdAt: '2023-01-15T10:30:00Z',
    isActive: true
  },
  {
    id: 2,
    name: 'Jane Smith',
    email: 'jane@example.com',
    role: 'user',
    avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=2',
    createdAt: '2023-02-20T14:22:00Z',
    isActive: true
  },
  // ... more users
];

export const mockUserProfiles = {
  1: {
    bio: 'Software engineer with 10 years of experience',
    location: 'San Francisco, CA',
    website: 'https://johndoe.dev',
    socialLinks: {
      twitter: '@johndoe',
      linkedin: 'john-doe-dev'
    }
  },
  // ... more profiles
};

Environment Configuration

Using Environment Variables

Create a .env file in your project root:

# Development
REACT_APP_USE_MOCKS=true
REACT_APP_API_URL=https://api.yourapp.com

# For Vue projects, use VITE_ prefix
VITE_USE_MOCKS=true
VITE_API_URL=https://api.yourapp.com

Conditional Mock Setup

// src/mocks/index.ts (Create React App / Webpack)
import { setupMocks, setupStatefulMocks, apiMocker } from './api-mocks';

const env = typeof process !== 'undefined' && process?.env ? process.env : {};

const config = {
  // Enable mocks in development or when explicitly requested
  enabled: env.NODE_ENV === 'development' || env.REACT_APP_USE_MOCKS === 'true',

  // Use stateful mocks for more realistic testing
  stateful: env.REACT_APP_STATEFUL_MOCKS === 'true',

  // Add delays to simulate real network conditions
  realistic: env.REACT_APP_REALISTIC_DELAYS === 'true'
};

if (config.enabled) {
  console.log('Initializing API mocks...');

  // Configure the mocker
  apiMocker.updateConfig({
    baseUrl: env.REACT_APP_API_URL,
    logRequests: true,
    globalDelay: config.realistic ? 500 : 0
  });

  // Setup mocks
  if (config.stateful) {
    setupStatefulMocks();
  } else {
    setupMocks();
  }

  console.log('API mocks ready');
} else {
  console.log('Using real API endpoints');
  apiMocker.disable();
}
// src/mocks/index.ts (Vite)
import { setupMocks, setupStatefulMocks, apiMocker } from './api-mocks';

const env = import.meta.env;

const config = {
  // Enable mocks in development or when explicitly requested
  enabled: env.DEV || env.MODE === 'development' || env.VITE_USE_MOCKS === 'true',

  // Use stateful mocks for more realistic testing
  stateful: env.VITE_STATEFUL_MOCKS === 'true',

  // Add delays to simulate real network conditions
  realistic: env.VITE_REALISTIC_DELAYS === 'true'
};

if (config.enabled) {
  console.log('Initializing API mocks...');

  // Configure the mocker
  apiMocker.updateConfig({
    baseUrl: env.VITE_API_URL,
    logRequests: true,
    globalDelay: config.realistic ? 500 : 0
  });

  // Setup mocks
  if (config.stateful) {
    setupStatefulMocks();
  } else {
    setupMocks();
  }

  console.log('API mocks ready');
} else {
  console.log('Using real API endpoints');
  apiMocker.disable();
}

Migration to Real APIs

When you're ready to connect to real APIs, you have several options:

1. Disable All Mocks

// Simply disable all mocking
apiMocker.disable();

2. Gradual Migration

// Remove specific endpoints as real ones become available
apiMocker.remove('GET', '/users');
apiMocker.remove('POST', '/users');

// Keep other mocks active
// apiMocker.get('/products', mockProducts); // Still mocked

3. Environment-based Control

// Use environment variables to control which endpoints are mocked (CRA / Webpack)
const env = typeof process !== 'undefined' && process?.env ? process.env : {};

const mockConfig = {
  users: env.REACT_APP_MOCK_USERS !== 'false',
  products: env.REACT_APP_MOCK_PRODUCTS !== 'false',
  orders: env.REACT_APP_MOCK_ORDERS !== 'false'
};

if (mockConfig.users) {
  setupUserMocks();
}

if (mockConfig.products) {
  setupProductMocks();
}

if (mockConfig.orders) {
  setupOrderMocks();
}
// Use environment variables to control which endpoints are mocked (Vite)
const env = import.meta.env;

const mockConfig = {
  users: env.VITE_MOCK_USERS !== 'false',
  products: env.VITE_MOCK_PRODUCTS !== 'false',
  orders: env.VITE_MOCK_ORDERS !== 'false'
};

if (mockConfig.users) {
  setupUserMocks();
}

if (mockConfig.products) {
  setupProductMocks();
}

if (mockConfig.orders) {
  setupOrderMocks();
}

API Reference

Creating a Mocker

import { ApiMocker, createMocker } from '@onamfc/api-mocker';

// Using the factory function (recommended)
const mocker = createMocker({
  baseUrl: 'https://api.example.com',
  globalDelay: 500,
  logRequests: true,
  globalHeaders: {
    'Content-Type': 'application/json',
    'X-Custom-Header': 'value'
  }
});

// Or using the class directly
const mocker = new ApiMocker(config);

Configuration Options

interface MockConfig {
  baseUrl?: string;           // Base URL to match against
  globalDelay?: number;       // Default delay for all requests (ms)
  globalHeaders?: Record<string, string>; // Default headers for all responses
  logRequests?: boolean;      // Enable/disable request logging
  defaultPriority?: number;   // Default priority for endpoints
}

Adding Mock Endpoints

Static Responses

// Using convenience methods
mocker
  .get('/users', [{ id: 1, name: 'John' }])
  .post('/users', { id: 2, name: 'Jane' })
  .put('/users/:id', { id: 1, name: 'Updated John' })
  .delete('/users/:id', { success: true });

// Using the generic mock method
mocker.mock({
  path: '/products',
  method: 'GET',
  response: [{ id: 1, name: 'Product 1' }],
  status: 200,
  delay: 1000,
  headers: { 'X-Total-Count': '1' }
});

Dynamic Responses

// Enhanced dynamic response with full context
mocker.mockDynamic({
  path: '/users/:id',
  method: 'GET',
  handler: (context) => {
    const userId = context.params.id;
    const includeEmail = context.queryParams.include === 'email';
    const isAdmin = context.headers['x-user-role'] === 'admin';
    
    return {
      id: userId,
      name: `User ${userId}`,
      email: includeEmail ? `user${userId}@example.com` : undefined,
      role: isAdmin ? 'admin' : 'user',
      timestamp: new Date().toISOString()
    };
  },
  delay: 500
});

// Stateful dynamic response
mocker.mockDynamic({
  path: '/analytics/pageview',
  method: 'POST',
  handler: (context) => {
    // Initialize analytics if not exists
    if (!context.state.analytics) {
      context.state.analytics = { pageViews: 0, uniqueVisitors: new Set() };
    }
    
    const { page, userId } = context.body;
    context.state.analytics.pageViews++;
    context.state.analytics.uniqueVisitors.add(userId);
    
    return {
      page,
      totalViews: context.state.analytics.pageViews,
      uniqueVisitors: context.state.analytics.uniqueVisitors.size
    };
  }
});

Managing Mocks

// Enable/disable mocking
mocker.enable();
mocker.disable();

// Clear all mocks
mocker.clear();

// Remove specific mock
mocker.remove('GET', '/users');

// State management
mocker.setState('key', 'value');
const state = mocker.getState();
mocker.clearState();

// Restore original fetch
mocker.restore();

// Update configuration
mocker.updateConfig({
  logRequests: false,
  globalDelay: 1000
});

Best Practices

  1. Organize Your Mocks: Keep mock definitions in separate files organized by feature or endpoint
  2. Use Environment Variables: Control mock behavior through environment variables
  3. Realistic Data: Use realistic mock data that matches your actual API responses
  4. Test Error Scenarios: Mock both success and error responses to test error handling
  5. Gradual Migration: Disable mocks endpoint by endpoint as real APIs become available
  6. Type Safety: Use TypeScript interfaces for your API responses
interface User {
  id: number;
  name: string;
  email: string;
}

mocker.get('/users', [] as User[]);
  1. State Management: Use stateful mocks for realistic CRUD operations
  2. Network Conditions: Use delays and network errors to test under realistic conditions

Troubleshooting

Common Issues

Mocks not working:

  • Check that mocks are initialized before your API calls
  • Verify the baseUrl matches your API calls
  • Ensure mocking is enabled (mocker.enable())

TypeScript errors:

  • Make sure you have proper type definitions for your mock data
  • Use as assertions when needed for complex types

State not persisting:

  • Remember that state is only maintained within the same session
  • Use mocker.setState() to initialize state if needed

Priority conflicts:

  • Use explicit priorities when you have overlapping endpoints
  • More specific paths automatically get higher priority

Development

npm install
npm test
npm run build

These commands run the Jest test suite (using the Node 18 runtime) and build the distributable bundles under dist/.

Contributing

Issues and pull requests are welcome! Please visit the GitHub repository.

About

A lightweight TypeScript library for mocking API endpoints during frontend development. Perfect for developing frontend applications before the backend is ready, or for testing different API scenarios.

Resources

License

Stars

Watchers

Forks

Packages

No packages published