Why do we need both Transform and Service?
Why Actix Middleware Has Two Layers
Simple Real-World Analogy
What Transform Does
What Service Does
Core Difference
Visual Flow
Now Actual Actix Middleware Example
Step-by-Step Theory of the Code
Simple Understanding of Left and Right Body
What Happens at Runtime
Objective and MCQ
When developers start building custom middleware in Actix-Web, one concept creates the most confusion:
Why do we need both Transform and Service?
At first, it feels like both are doing the same job. But in reality, they have different responsibilities.
This is the core idea:
Transform creates the middleware
Service runs the middleware logic for each request
Once you understand this two-layer architecture, custom middleware becomes much easier.
Why Actix Middleware Has Two Layers
Actix middleware is designed in a flexible and reusable way.
A middleware must do two separate things:
Layer 1: Build the middleware
This happens when Actix sets up your app.
Layer 2: Process every incoming request
This happens every time a user calls an API route.
Because these are two different jobs, Actix separates them into:
Transform
Service
Simple Real-World Analogy
Think of a security system in an office.
Transform
This is the company that installs the security gate.
Service
This is the security guard who checks every person entering.
So:
the gate is created once
the checking happens again and again for each visitor
That is exactly how middleware works in Actix.
What Transform Does
Transform is the middleware factory.
Its job is:
receive the next inner service
wrap it
return a new middleware service
In simple words:
Transform prepares the middleware structure.
It does not handle every request directly.
Basic Theory of Transform
When you use:
.wrap(AuthMiddleware)
Actix calls Transform behind the scenes.
It says:
“Here is the inner service. Build a middleware around it.”
So Transform is like a constructor or factory.
2.
What Service Does
Service is the real request processor.
Its job is:
receive each request
inspect it
decide whether to block or allow
call the next service if allowed
In simple words:
Service contains the actual middleware logic.
This is where you write code like:
check session
check token
check role
log request
measure response time
Core Difference
Transform
runs when middleware is attached
creates the middleware wrapper
setup layer
Service
runs on every request
executes middleware logic
runtime layer
Visual Flow
App Startup
↓
Transform runs
↓
Middleware service created
↓
-----------------------------
Each Client Request
↓
Service runs
↓
Check request
↓
Allow or block
- A Very Simple Rust Example First
Before Actix, let us understand the idea using plain Rust thinking.
Factory example
struct GateFactory;
struct Gate;
impl GateFactory {
fn build(&self) -> Gate {
Gate
}
}
impl Gate {
fn check(&self, name: &str) {
println!("Checking entry for {}", name);
}
}
fn main() {
let factory = GateFactory;
let gate = factory.build();
gate.check("Ashwani");
gate.check("Ravi");
}
Output
Checking entry for Ashwani
Checking entry for Ravi
Explanation
GateFactory = like Transform
Gate = like Service
Factory creates the gate once.
Gate checks people many times.
That is the same idea as Actix middleware.
Now Actual Actix Middleware Example
Below is a basic custom authentication middleware.
It checks whether request contains header x-auth-token.
If header is missing:
return 401 Unauthorized
If present:
allow request to continue
Full Middleware Code
use std::future::{ready, Ready};
use std::rc::Rc;
use actix_web::{
body::{EitherBody, MessageBody},
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
Error, HttpResponse,
};
use futures_util::future::LocalBoxFuture;
pub struct AuthMiddleware;
impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
B: MessageBody + 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type InitError = ();
type Transform = AuthMiddlewareService<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(AuthMiddlewareService {
service: Rc::new(service),
}))
}
}
pub struct AuthMiddlewareService<S> {
service: Rc<S>,
}
impl<S, B> Service<ServiceRequest> for AuthMiddlewareService<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
B: MessageBody + 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let service = self.service.clone();
Box::pin(async move {
let has_token = req.headers().contains_key("x-auth-token");
if !has_token {
let response = HttpResponse::Unauthorized().body("Missing auth token");
return Ok(req.into_response(response).map_into_right_body());
}
let response = service.call(req).await?;
Ok(response.map_into_left_body())
})
}
}
Step-by-Step Theory of the Code
Part 1: Middleware marker struct
pub struct AuthMiddleware;
This is the middleware type you attach with:
.wrap(AuthMiddleware)
This struct does not contain logic itself.
It simply represents the middleware.
Part 2: Transform implementation
impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware
This means:
AuthMiddleware can transform an inner service into a middleware-wrapped service.
Here:
S = inner service
B = body type
Part 3: new_transform()
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(AuthMiddlewareService {
service: Rc::new(service),
}))
}
This is the most important Transform function.
What it does
receives the inner service
wraps it inside AuthMiddlewareService
returns it
So this is the creation layer.
This happens when Actix builds the middleware pipeline.
Part 4: middleware service struct
pub struct AuthMiddlewareService<S> {
service: Rc<S>,
}
This struct stores the inner service.
After middleware check passes, request will be forwarded to this inner service.
Part 5: Service implementation
impl<S, B> Service<ServiceRequest> for AuthMiddlewareService<S>
This means:
AuthMiddlewareService behaves like a service that can process requests.
This is the runtime layer.
Part 6: call() method
fn call(&self, req: ServiceRequest) -> Self::Future
This method runs on every request.
This is where the actual middleware logic is written.
Part 7: request checking logic
let has_token = req.headers().contains_key("x-auth-token");
Middleware checks whether request has authentication token.
Part 8: block unauthorized request
if !has_token {
let response = HttpResponse::Unauthorized().body("Missing auth token");
return Ok(req.into_response(response).map_into_right_body());
}
If token missing:
middleware stops request
returns 401 Unauthorized
This is middleware-generated response.
So it uses:
map_into_right_body()
Part 9: allow request to continue
let response = service.call(req).await?;
Ok(response.map_into_left_body())
If token exists:
middleware forwards request to inner service
handler runs normally
This is handler-generated response.
So it uses:
map_into_left_body()
Simple Understanding of Left and Right Body
In middleware there can be two response sources:
Left body
Normal handler response
Right body
Middleware custom response
So remember:
map_into_left_body() = request allowed
map_into_right_body() = request blocked by middleware
- How to Use This Middleware in main.rs
use actix_web::{web, App, HttpResponse, HttpServer, Responder};
mod auth_middleware;
use auth_middleware::AuthMiddleware;
async fn dashboard() -> impl Responder {
HttpResponse::Ok().body("Welcome to dashboard")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.service(
web::scope("/api")
.wrap(AuthMiddleware)
.route("/dashboard", web::get().to(dashboard))
)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
What Happens at Runtime
App startup
When application starts:
.wrap(AuthMiddleware)
Actix triggers Transform.
So:
Transform creates AuthMiddlewareService
middleware pipeline is prepared
Request 1: No token
User sends request without header:
GET /api/dashboard
Middleware Service runs:
checks header
header missing
returns 401
Output
Missing auth token
Request 2: With token
User sends request with header:
x-auth-token: abc123
Middleware Service runs:
checks header
token exists
forwards request to handler
Handler returns:
Welcome to dashboard
- Output Summary
Without token
Status: 401 Unauthorized
Body: Missing auth token
With token
Status: 200 OK
Body: Welcome to dashboard
- Why Beginners Get Confused
Many beginners think middleware is only one thing.
They expect:
one struct
one trait
one request handler
But Actix separates middleware into two levels:
Level 1: Creation
Handled by Transform
Level 2: Execution
Handled by Service
This design gives Actix great flexibility.
- Interview-Style Answer
If someone asks in interview:
What is the difference between Transform and Service in Actix middleware?
You can answer like this:
In Actix custom middleware, Transform acts as a factory that creates the middleware wrapper around the inner service. It runs when the application pipeline is built. Service contains the actual middleware logic and runs on every incoming request. Transform is the setup layer, while Service is the execution layer.
Objective and MCQ
What is the main purpose of middleware in Actix-web?
A. Handle database queries
B. Intercept requests before they reach handlers
C. Compile Rust code faster
D. Replace HTTP responses
✅ Answer: B
Middleware can inspect or reject requests before they reach handlers.
- Middleware in Actix-web is typically implemented using which two traits?
A. Future and Async
B. Transform and Service
C. Handler and Request
D. Router and Scope
✅ Answer: B
Middleware is created using Transform (factory) and Service (execution) traits.
- Which Actix type represents an incoming HTTP request inside middleware?
A. HttpRequest
B. ServiceRequest
C. RequestContext
D. RequestBody
✅ Answer: B
ServiceRequest contains the request data processed by middleware.
- Which type represents the response returned from middleware or handlers?
A. HttpResponse
B. ServiceResponse
C. ResponseBody
D. HandlerResponse
✅ Answer: B
ServiceResponse wraps both request and response objects.
- What happens if middleware decides to block a request?
A. Request continues to handler
B. Middleware returns its own HTTP response
C. Server restarts
D. Request is ignored
✅ Answer: B
Middleware can stop processing and return an early response.
- Which HTTP response is commonly returned when authentication fails?
A. 200 OK
B. 401 Unauthorized
C. 201 Created
D. 302 Redirect
✅ Answer: B
- Which method in middleware processes each incoming request?
A. execute()
B. handle()
C. call()
D. run()
✅ Answer: C
The call() function is where request processing occurs.
- What does forward_ready! macro do in middleware?
A. Sends response to client
B. Forwards readiness check to inner service
C. Creates a request object
D. Serializes JSON
✅ Answer: B
- Why does middleware often use generics like ?
A. To support different services and response body types
B. To reduce code size
C. To store database values
D. To handle logging
✅ Answer: A
Generics allow middleware to work with any service and body type.
- What does S usually represent in middleware generic parameters?
A. Session object
B. Inner service being wrapped
C. Server configuration
D. Security token
✅ Answer: B
- Which trait must response body type implement in Actix middleware?
A. Clone
B. Serialize
C. MessageBody
D. Future
✅ Answer: C
- What does the middleware authentication function typically check?
A. Database schema
B. Request headers or session tokens
C. Compiler version
D. JSON parsing
✅ Answer: B
- What architectural pattern describes middleware execution order?
A. MVC pattern
B. Onion / pipeline pattern
C. Observer pattern
D. Singleton pattern
✅ Answer: B
Middleware layers wrap around handlers like an onion structure.
- In route protection middleware, what usually happens when the user is authenticated?
A. Request stops
B. Middleware calls the inner service
C. Server logs out user
D. Database connection closes
✅ Answer: B
- Which Actix function allows attaching middleware to an application?
A. attach()
B. wrap()
C. register()
D. bind()
✅ Answer: B
App::wrap() attaches middleware.
- Which type is commonly used to represent asynchronous middleware results?
A. Result
B. LocalBoxFuture
C. AsyncResponse
D. FutureResult
✅ Answer: B
- What is the role of Transform trait in middleware?
A. Handle request execution
B. Create middleware service instances
C. Manage database connections
D. Serialize responses
✅ Answer: B
- Which scenario is a common use case for route protection middleware?
A. Image processing
B. Authentication and authorization
C. File compression
D. Code compilation
✅ Answer: B
- Which middleware logic is typically executed before the handler?
A. Response serialization
B. Authentication check
C. Database backup
D. File upload
✅ Answer: B
- What is the main advantage of middleware-based route protection?
A. Reduces database size
B. Centralizes authentication logic
C. Removes need for handlers
D. Increases CPU usage
✅ Answer: B
Middleware centralizes authentication and security checks for multiple routes.
Top comments (0)