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:
- Chaining: Each middleware function should be able to call the next middleware in the chain.
- Request/Response Modification: Middleware functions should be able to inspect, modify, or augment the request object and the response object.
- Asynchronous Operations: Middleware functions can perform asynchronous operations (e.g., fetching data, making another API call).
- Stopping the Chain: A middleware function should have the ability to stop the execution of subsequent middleware and return a response immediately.
- Error Handling: The system should gracefully handle errors thrown by middleware functions.
- 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:
middlewarefunction:- Accepts an array of
Middlewarefunctions. - Returns a
Handlerfunction.
- Accepts an array of
Middlewarefunction 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. Ifnextis not called, the chain stops.- Returns
Responseif the middleware decides to handle the response and stop the chain. Returnsvoidif it wants to pass control to the next middleware.
Handlerfunction signature:(request: Request) => Promise<Response>- This function, returned by
middleware, will initiate the middleware chain. It takes an initialRequestobject and should return the finalResponseobject.
- This function, returned by
Requestobject: A simple object that can holdurl,method,headers,body, and any other custom properties.Responseobject: A simple object that can holdstatus,data, and any other custom properties.- Error Handling: If any middleware throws an error, the
Handlershould catch it and return aResponsewith 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 aResponse. (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/awaitsyntax 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 aResponse. The currentcreateMiddlewareSystemattempts 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
RequestandResponseinterfaces provided are basic. In a real-world scenario, they would likely be more extensive (e.g., includingRequestInitoptions forfetch). - This challenge focuses on the middleware system itself. You don't need to build a full React UI, but you should show how the
handlerfunction would be invoked from a React context (as shown in the examples).