Hone logo
Hone
Problems

React Middleware System for Request Handling

This challenge requires you to implement a flexible middleware system within a React application to manage and intercept asynchronous requests. This system will allow you to chain together various functions that can modify requests, handle responses, or perform side effects before or after the actual API call. This is a common pattern in modern web development for tasks like authentication, logging, error handling, and data transformation.

Problem Description

You need to build a custom middleware function that takes an array of middleware functions and returns a new function. This returned function will be responsible for executing the middleware chain and handling the final request.

Your middleware system should support the following:

  1. Chaining: Each middleware function should be able to call the next middleware in the chain.
  2. Request/Response Modification: Middleware functions should be able to inspect, modify, or augment the request object and the response object.
  3. Asynchronous Operations: Middleware functions can perform asynchronous operations (e.g., fetching data, making another API call).
  4. Stopping the Chain: A middleware function should have the ability to stop the execution of subsequent middleware and return a response immediately.
  5. Error Handling: The system should gracefully handle errors thrown by middleware functions.
  6. Integration with React: The middleware system should be designed to be easily integrated into a React component or a custom hook for making API calls.

Key Requirements:

  • middleware function:
    • Accepts an array of Middleware functions.
    • Returns a Handler function.
  • Middleware function signature: (request: Request, next: NextMiddleware) => Promise<Response | void>
    • request: An object representing the current request, which can be modified.
    • next: A function to call to execute the next middleware in the chain. If next is not called, the chain stops.
    • Returns Response if the middleware decides to handle the response and stop the chain. Returns void if it wants to pass control to the next middleware.
  • Handler function signature: (request: Request) => Promise<Response>
    • This function, returned by middleware, will initiate the middleware chain. It takes an initial Request object and should return the final Response object.
  • Request object: A simple object that can hold url, method, headers, body, and any other custom properties.
  • Response object: A simple object that can hold status, data, and any other custom properties.
  • Error Handling: If any middleware throws an error, the Handler should catch it and return a Response with an appropriate error status and message.

Expected Behavior:

When the Handler function is called with an initial Request, it should sequentially execute each Middleware function. Each Middleware receives the current Request object and a next function. If a Middleware calls next(), the chain continues with the next Middleware and the potentially modified Request. If a Middleware returns a Response object (or doesn't call next()), the chain is terminated, and that Response is returned by the Handler.

Edge Cases:

  • An empty middleware array.
  • Middleware functions that throw synchronous or asynchronous errors.
  • Middleware that doesn't call next() and doesn't return a Response. (This should be treated as an implicit stop, but ideally, it should lead to a default response or error).

Examples

Example 1: Basic Logging and Authentication

interface Request {
  url: string;
  method: string;
  headers: Record<string, string>;
  body?: any;
}

interface Response {
  status: number;
  data?: any;
  error?: string;
}

type NextMiddleware = () => Promise<Response | void>;

type Middleware = (request: Request, next: NextMiddleware) => Promise<Response | void>;

// --- Middleware Functions ---

const loggerMiddleware: Middleware = async (request, next) => {
  console.log(`[Request Log] ${request.method} ${request.url}`);
  const result = await next();
  console.log(`[Response Log] Status: ${result?.status || 'N/A'}`);
  return result;
};

const authMiddleware: Middleware = async (request, next) => {
  if (!request.headers['Authorization']) {
    console.warn('Authentication token missing!');
    return { status: 401, error: 'Unauthorized' };
  }
  // In a real app, you'd validate the token
  console.log('Authentication successful (simulated).');
  return await next();
};

const jsonBodyParser: Middleware = async (request, next) => {
  if (request.body && typeof request.body === 'string' && request.headers['Content-Type'] === 'application/json') {
    try {
      request.body = JSON.parse(request.body);
    } catch (e) {
      return { status: 400, error: 'Invalid JSON body' };
    }
  }
  return await next();
};

// --- Mock API Call ---
const mockApiCall = (request: Request): Promise<Response> => {
  return new Promise((resolve) => {
    setTimeout(() => {
      if (request.url === '/users' && request.method === 'GET') {
        resolve({ status: 200, data: [{ id: 1, name: 'Alice' }] });
      } else if (request.url === '/users' && request.method === 'POST') {
        resolve({ status: 201, data: { id: 2, ...request.body } });
      } else {
        resolve({ status: 404, error: 'Not Found' });
      }
    }, 100);
  });
};

// --- Middleware System Implementation ---
function createMiddlewareSystem(middlewares: Middleware[]) {
  return function handler(initialRequest: Request): Promise<Response> {
    const executeChain = async (request: Request, index: number): Promise<Response | void> => {
      if (index >= middlewares.length) {
        // Base case: End of middleware chain, perform actual request
        return mockApiCall(request);
      }

      const currentMiddleware = middlewares[index];
      const nextMiddleware = () => executeChain(request, index + 1);

      try {
        const result = await currentMiddleware(request, nextMiddleware);
        if (result !== undefined) {
          // Middleware returned a response, stop the chain
          return result;
        }
      } catch (error: any) {
        console.error(`Error in middleware at index ${index}:`, error);
        return { status: 500, error: error.message || 'An unexpected error occurred' };
      }
      // If middleware returned undefined, continue to next.
      // If executeChain returns undefined (meaning no middleware or base case returned a response),
      // we need to ensure a Response is eventually returned.
      // This logic might need refinement depending on how you want to handle a chain that never resolves to a Response.
      // For now, we assume the base case always returns a Response or an error will.
      return undefined; // Explicitly return undefined if no response is determined yet.
    };

    // Start the chain execution
    const finalResult = executeChain(initialRequest, 0);

    // Ensure a Response is always returned
    return finalResult.then(res => res || { status: 500, error: 'Middleware chain did not resolve to a response' });
  };
}

// --- Usage ---
const apiHandler = createMiddlewareSystem([loggerMiddleware, authMiddleware, jsonBodyParser]);

async function fetchData() {
  const request1: Request = {
    url: '/users',
    method: 'GET',
    headers: { 'Authorization': 'Bearer token123' },
  };
  console.log('\n--- Fetching Data ---');
  const response1 = await apiHandler(request1);
  console.log('Response 1:', response1);

  const request2: Request = {
    url: '/users',
    method: 'POST',
    headers: {
      'Authorization': 'Bearer token123',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ name: 'Bob', email: 'bob@example.com' }),
  };
  console.log('\n--- Creating User ---');
  const response2 = await apiHandler(request2);
  console.log('Response 2:', response2);

  const request3: Request = {
    url: '/users',
    method: 'GET',
    headers: {}, // Missing Authorization
  };
  console.log('\n--- Fetching Data (Unauthorized) ---');
  const response3 = await apiHandler(request3);
  console.log('Response 3:', response3);
}

fetchData();

Explanation:

The loggerMiddleware logs the request details and the final response status. The authMiddleware checks for an Authorization header and returns a 401 if it's missing, stopping the chain. The jsonBodyParser attempts to parse a JSON string body. The createMiddlewareSystem function orchestrates the execution. The handler returned by createMiddlewareSystem starts the chain. When next() is called in a middleware, it progresses to the next one. If a middleware returns a Response (like authMiddleware does), the Handler returns that Response. If all middleware call next(), the mockApiCall is invoked.

Example 2: Error Propagation and Default Response

// Using the same Request, Response, NextMiddleware, and Middleware types from Example 1.

// --- Middleware Functions ---
const errorProneMiddleware: Middleware = async (request, next) => {
  console.log('Executing errorProneMiddleware...');
  // Simulate an intermittent error
  if (Math.random() > 0.5) {
    throw new Error('Something went wrong in this middleware!');
  }
  return await next();
};

const delayMiddleware: Middleware = async (request, next) => {
  console.log('Executing delayMiddleware...');
  await new Promise(resolve => setTimeout(resolve, 50)); // Simulate delay
  return await next();
};

const finalResponseMiddleware: Middleware = async (request, next) => {
    console.log('Executing finalResponseMiddleware...');
    // This middleware intentionally doesn't call next() but returns a response
    return { status: 200, data: { message: 'Handled by final middleware' } };
};

// --- Mock API Call (will not be reached if finalResponseMiddleware is active) ---
const mockApiCall = (request: Request): Promise<Response> => {
  console.log('Performing actual API call...');
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ status: 200, data: 'API Success' });
    }, 100);
  });
};

// --- Middleware System Implementation (re-using createMiddlewareSystem from Example 1) ---
// Assuming createMiddlewareSystem is defined as in Example 1.

// --- Usage ---
const errorHandlingHandler = createMiddlewareSystem([
  errorProneMiddleware,
  delayMiddleware,
  finalResponseMiddleware // This one will stop the chain
]);

async function handleErrors() {
  const request: Request = {
    url: '/test',
    method: 'GET',
    headers: {},
  };

  console.log('\n--- Testing Error Handling ---');
  const response = await errorHandlingHandler(request);
  console.log('Response:', response);

  // Example where errorProneMiddleware might throw
  console.log('\n--- Testing Another Call (potential error) ---');
  const response2 = await errorHandlingHandler(request);
  console.log('Response 2:', response2);
}

handleErrors();

Explanation:

In this example, errorProneMiddleware might throw an error, which is caught by the createMiddlewareSystem's error handling. finalResponseMiddleware is placed last and intentionally does not call next(). It directly returns a Response. This demonstrates how a middleware can short-circuit the chain and provide a final result without needing to reach the actual API call.

Constraints

  • Number of Middleware Functions: The system should be able to handle up to 50 middleware functions in a single chain.
  • Request/Response Payload Size: Assume request and response payloads (body) will not exceed 1MB.
  • Asynchronous Operations: Middleware functions should be designed to handle async/await syntax efficiently.
  • React Integration: The solution should be implementable within a typical React functional component or a custom hook context.
  • TypeScript: The entire solution, including types, must be written in TypeScript.

Notes

  • Consider how you want to handle a scenario where a middleware doesn't call next() and also doesn't return a Response. The current createMiddlewareSystem attempts to return a default error response in such cases.
  • Think about how you would adapt this to a React custom hook that manages the state of requests (loading, error, data).
  • The Request and Response interfaces provided are basic. In a real-world scenario, they would likely be more extensive (e.g., including RequestInit options for fetch).
  • This challenge focuses on the middleware system itself. You don't need to build a full React UI, but you should show how the handler function would be invoked from a React context (as shown in the examples).
Loading editor...
typescript