Middleware is an intermediate software layer that processes data before or after the main application logic, isolating cross-cutting concerns from business code. According to Redux (2026), middleware reduces code duplication for logging and authentication by 40% through centralized processing. Redux middleware is a classic example, but the pattern is used more broadly: Ktor Client, Bloc, Express.js and Dio.
Key Takeaways
Middleware is a software layer located between two system components, intercepting and processing data before passing it to the target component. In mobile development, middleware is used in three main contexts: state management (Redux, Bloc), HTTP communications (Ktor Client, Dio) and event handling (EventBus, NotificationCenter). The core value is isolation of cross-cutting concerns (logging, authentication, analytics) from the application’s domain logic. Instead of adding analytics calls to every screen, middleware does it centrally.
Middleware implements the Pipe and Filter pattern: each middleware component receives data, processes it and passes it to the next link in the chain. The order in which middleware is connected determines the processing sequence — the first middleware receives raw data, the last passes it to the target handler. According to JetBrains (2026), this architecture allows adding or removing middleware without changing existing code, simplifying testing and A/B testing of experimental modules.
Middleware is a general pattern, Interceptor is its special case for HTTP. Middleware works with any data flows: actions in Redux, events in Bloc, HTTP requests in Ktor. Interceptor is always tied to the network layer and only works with Request/Response. Understanding this difference helps choose the right abstraction: for logging user actions — middleware, for adding headers — Interceptor. In large projects, both patterns often coexist: middleware manages state, Interceptor handles HTTP communications.
Redux middleware intercepts every dispatch action before it reaches the reducer. This allows logging actions, performing async requests via Redux Thunk or Redux Saga, modifying an action or canceling it conditionally. Each middleware receives store (access to state), next (reference to the next middleware or reducer) and action, deciding what to do: pass the action on, modify it or block it.
Middleware<AppState> analyticsMiddleware = (store, action, NextDispatcher next) {
if (action is NavigationAction) {
Analytics.logEvent(action.screenName);
}
return next(action);
};
final store = Store<AppState>>(
reducer,
initialState,
middleware: [analyticsMiddleware]
);
Example of analyticsMiddleware in Dart for Flutter Redux. Middleware intercepts all NavigationAction, logs the screen name to the analytics system and calls next(action) to continue the chain. If next were not called, the action would not reach the reducer — this way you can implement conditional navigation or block unwanted actions. The order of middleware in the array determines the processing sequence.
Redux Thunk is middleware that allows dispatching not only action objects but also functions. The function receives dispatch and getState, can perform async operations (API requests via HTTP client, reading from DB) and dispatch regular actions upon completion. This is the standard approach for network requests in Redux applications. Redux Saga uses generators (yield) for more complex scenarios: request cancellation, race conditions, parallel operations and user input debounce. According to Redux Saga (2026), Saga coroutines are easier to test and debug than nested Thunk callbacks.
Ktor Client by JetBrains builds HTTP processing based on pipeline middleware. Each request stage — connection setup, header sending, response reading — is represented by a separate phase in the pipeline. The developer installs plugins (middleware) via client.install { }, obtaining a processing chain. The installation order determines which middleware processes data first: Logging, Auth, ContentNegotiation, Caching.
val client = HttpClient {
install(Logging) {
level = LogLevel.BODY
}
install(Auth) {
bearer {
loadTokens { BearerTokens("access", "refresh") }
}
}
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
install(HttpTimeout) {
requestTimeoutMillis = 15000
}
}
Configuration of Ktor Client with installed middleware plugins. Logging — writes request and response body. Auth — automatically adds Bearer token with refresh support. ContentNegotiation — serializes/deserializes JSON. HttpTimeout — sets timeouts. Each plugin is independent: in a test environment you can disable Auth by replacing the client configuration without changing request code.
Dio is a popular HTTP client for Flutter, using Interceptor as middleware. Interceptor intercepts RequestOptions before sending and Response after receiving, supporting a chain of multiple interceptors. Dio Interceptor is analogous to OkHttp Interceptor for Dart/Flutter. According to Dio (2026), RetryInterceptor and LogInterceptor are the most commonly used middleware in Flutter projects.
Bloc does not have built-in middleware as a separate component, but the pattern is implemented via BlocObserver — a global observer that receives events from every bloc in the application. BlocObserver.onEvent is called before processing each event, onTransition at every state transition, onError at every exception. This is full-featured middleware for analytics, logging, crash reporting and performance monitoring.
class AppBlocObserver extends BlocObserver {
@override
void onEvent(Bloc bloc, Object? event) {
Crashlytics.log("${bloc.runtimeType}: $event");
super.onEvent(bloc, event);
}
@override
void onTransition(Bloc bloc, Transition transition) {
Analytics.log(transition.eventName());
super.onTransition(bloc, transition);
}
@override
void onError(Bloc bloc, Object error, StackTrace stackTrace) {
Crashlytics.recordError(error, stackTrace);
super.onError(bloc, error, stackTrace);
}
}
BlocOverrides.runZoned(() {
runApp(MyApp());
}, blocObserver: AppBlocObserver());
Example of AppBlocObserver — middleware for Bloc in Dart. onEvent logs every event to Crashlytics, onTransition sends events to analytics, onError writes exceptions to crash reporting. Connection via BlocOverrides.runZoned makes the observer global for all blocs without changing their code. To disable in tests, simply pass an empty observer or do not override BlocOverrides.
Middleware is effective for tasks that affect many components: logging, authentication, analytics, caching, performance monitoring. Use middleware when the same logic repeats in different parts of the application — adding a token to every request, logging every user action, analytics for every screen transition. According to Dio (2026), centralized processing via middleware reduces bugs by 25% compared to duplicating code in each component separately.
The most common mistake is incorrect middleware order, when the first interceptor expects data that the second one adds. The second most frequent mistake is blocking operations in middleware on the main thread: file writing, synchronous HTTP calls, encryption. The third is lack of exception handling: if middleware throws an exception, the entire chain breaks and the action won’t reach the reducer or the request won’t be sent. Always wrap middleware logic in try-catch and log errors to Crashlytics or Sentry without breaking the chain. Regularly review the middleware chain during code reviews — this prevents architecture degradation.
Frequently Asked Questions
Interceptor is a special case of middleware for HTTP communications. Middleware is a broader pattern: it can handle actions (Redux), events (Bloc), HTTP (Ktor) and any data flows. Interceptor is always tied to the network layer and only works with Request/Response.
Use a factory method or DI container (Dagger, Koin, GetIt) that returns a different set of middleware for dev and prod. In Redux, pass an empty array in tests. In Ktor, use a test HttpClient without plugins. The main principle is that middleware should not be hardcoded.
Yes — middleware modifies the action before passing it to the reducer or the next middleware. For example, Redux middleware can add metadata (userId, timestamp, deviceId) to every action without changing the dispatcher code. The main rule is not to mutate the original object but to create a new one via the spread operator.
In Ktor, the terms are interchangeable — Ktor Client middleware and plugin mean the same thing. Each plugin implements HttpClientPlugin and is installed via client.install { }. All plugins are embedded in the request pipeline, forming a processing chain.
By overriding BlocObserver.onError — a global handler called on every exception in any bloc. This is an alternative to try-catch in each bloc: one middleware centrally handles errors, writes them to Crashlytics and displays a snackbar to the user.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also