Middleware wraps around your tasks and resources, adding cross-cutting concerns like caching, retry logic, timeouts, and more—without cluttering your business logic.
What is Middleware?
Middleware is a function that intercepts task or resource execution, allowing you to:
Add behavior before execution (validation, logging)
Modify or transform inputs
Add behavior after execution (caching, cleanup)
Handle errors (retry, circuit breaking)
Short-circuit execution (caching, rate limiting)
Think of middleware as layers wrapped around your core logic, forming an “onion” where each layer can add functionality.
How Middleware Works
Middleware runs in the order you define it, wrapping your task or resource:
import { r } from "@bluelibs/runner" ;
const myTask = r
. task ( "app.tasks.example" )
. middleware ([
loggingMiddleware , // Runs first (outer layer)
authMiddleware , // Runs second
cachingMiddleware , // Runs third (inner layer)
])
. run ( async ( input ) => {
// Your business logic runs last
return processData ( input );
})
. build ();
Execution flow:
loggingMiddleware starts → logs “starting”
authMiddleware starts → checks permissions
cachingMiddleware starts → checks cache
Your task runs → processes data
cachingMiddleware ends → stores in cache
authMiddleware ends
loggingMiddleware ends → logs “completed”
Built-in Middleware
Runner provides production-ready middleware out of the box:
Retry Automatically retry failed operations with exponential backoff
Timeout Prevent operations from hanging indefinitely
Circuit Breaker Fail fast when downstream services are unavailable
Cache Cache expensive operations with automatic invalidation
Rate Limit Limit execution frequency to prevent overload
Custom Create your own middleware for specialized needs
Additional Built-in Middleware
Runner also includes specialized middleware for advanced use cases:
Concurrency (globals.middleware.task.concurrency) - Limit concurrent executions with semaphores
Fallback (globals.middleware.task.fallback) - Provide backup value or task if primary fails
Debounce (globals.middleware.task.debounce) - Delay execution until quiet period ends
Throttle (globals.middleware.task.throttle) - Ensure execution at most once per time window
Require Context (globals.middleware.task.requireContext) - Enforce async context availability
See Custom Middleware for examples and the API Reference for full configuration details.
Quick Example
Here’s how to add retry and timeout to an API call:
import { r , globals } from "@bluelibs/runner" ;
const callExternalAPI = r
. task ( "api.external" )
. middleware ([
globals . middleware . task . retry . with ({ retries: 3 }),
globals . middleware . task . timeout . with ({ ttl: 5000 }), // 5 second timeout
])
. run ( async ( url : string ) => {
const response = await fetch ( url );
if ( ! response . ok ) throw new Error ( `HTTP ${ response . status } ` );
return response . json ();
})
. build ();
This task will:
Retry up to 3 times on failure
Timeout after 5 seconds
Use exponential backoff between retries
Task vs Resource Middleware
Middleware comes in two flavors:
Task Middleware
Wraps task execution. Most built-in middleware (retry, cache, timeout) are task middleware.
const taskMiddleware = r . middleware
. task ( "app.middleware.logging" )
. run ( async ({ task , next , journal }, deps , config ) => {
console . log ( `Task ${ task . definition . id } starting` );
const result = await next ( task . input );
console . log ( `Task ${ task . definition . id } completed` );
return result ;
})
. build ();
Resource Middleware
Wraps resource initialization. Useful for lifecycle management and configuration.
const resourceMiddleware = r . middleware
. resource ( "app.middleware.monitoring" )
. run ( async ({ resource , next }, deps , config ) => {
console . log ( `Initializing ${ resource . definition . id } ` );
const value = await next ( resource . config );
console . log ( `Initialized ${ resource . definition . id } ` );
return value ;
})
. build ();
Global Middleware
Apply middleware to all tasks automatically:
import { r , globals } from "@bluelibs/runner" ;
const loggingMiddleware = r . middleware
. task ( "app.middleware.logging" )
. everywhere (() => true ) // Apply to all tasks
. dependencies ({ logger: globals . resources . logger })
. run ( async ({ task , next }, { logger }) => {
await logger . info ( `Executing: ${ task . definition . id } ` );
const result = await next ( task . input );
await logger . info ( `Completed: ${ task . definition . id } ` );
return result ;
})
. build ();
const app = r
. resource ( "app" )
. register ([ loggingMiddleware ]) // Automatically applies to all tasks
. build ();
Global middleware can depend on resources or tasks. Any such dependencies are excluded from the middleware’s execution to prevent infinite loops.
The Execution Journal
Middleware can share state using the execution journal—a type-safe registry that travels with each execution:
import { r , journal } from "@bluelibs/runner" ;
// Define a typed key
const traceIdKey = journal . createKey < string >( "app.traceId" );
const traceMiddleware = r . middleware
. task ( "app.middleware.trace" )
. run ( async ({ task , next , journal }) => {
// Store trace ID for other middleware/tasks
journal . set ( traceIdKey , `trace- ${ Date . now () } ` );
return next ( task . input );
})
. build ();
const myTask = r
. task ( "app.tasks.example" )
. middleware ([ traceMiddleware ])
. run ( async ( input , deps , { journal }) => {
// Read trace ID set by middleware
const traceId = journal . get ( traceIdKey ); // Fully typed!
console . log ( "Trace ID:" , traceId );
return { success: true };
})
. build ();
By default, journal.set() throws if a key already exists. Use { override: true } to intentionally update a value.
Middleware Configuration
Middleware can accept typed configuration:
type AuthConfig = { requiredRole : string };
const authMiddleware = r . middleware
. task ( "app.middleware.auth" )
. run ( async ({ task , next }, deps , config : AuthConfig ) => {
if ( task . input . user . role !== config . requiredRole ) {
throw new Error ( "Insufficient permissions" );
}
return next ( task . input );
})
. build ();
// Apply with configuration
const adminTask = r
. task ( "app.tasks.adminOnly" )
. middleware ([ authMiddleware . with ({ requiredRole: "admin" })])
. run ( async ( input ) => "Secret admin data" )
. build ();
Best Practices
Middleware runs in the order you define. Place logging/tracing first, caching last: . middleware ([
loggingMiddleware , // First: logs everything
authMiddleware , // Second: checks permissions
cacheMiddleware , // Last: serves from cache if available
])
Use the journal for coordination
Export journal keys so middleware can coordinate: export const journalKeys = {
userId: journal . createKey < string >( "auth.userId" ),
};
Each middleware should do one thing well. Combine multiple middleware instead of creating complex ones.
Handle errors appropriately
Middleware can catch and transform errors: try {
return await next ( task . input );
} catch ( error ) {
// Log, transform, or rethrow
throw new CustomError ( "Operation failed" , error );
}
Next Steps
Retry Middleware Handle transient failures automatically
Cache Middleware Speed up expensive operations
Custom Middleware Build your own middleware
Circuit Breaker Protect against cascading failures