Debug School

rakesh kumar
rakesh kumar

Posted on

Understanding the Two-Layer Architecture of Actix Middleware: Transform vs Service

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
Enter fullscreen mode Exit fullscreen mode

Simple Real-World Analogy

Think of a security system in an office.

Transform

This is the company that installs the security gate.

Enter fullscreen mode Exit fullscreen mode

Service

This is the security guard who checks every person entering.
Enter fullscreen mode Exit fullscreen mode

So:

the gate is created once

the checking happens again and again for each visitor
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In simple words:

Transform prepares the middleware structure.

It does not handle every request directly.

Basic Theory of Transform

When you use:


.wrap(AuthMiddleware)
Enter fullscreen mode Exit fullscreen mode

Actix calls Transform behind the scenes.

It says:

“Here is the inner service. Build a middleware around it.”
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In simple words:

Service contains the actual middleware logic.

This is where you write code like:

check session

check token

check role

log request
Enter fullscreen mode Exit fullscreen mode

measure response time

Core Difference

Transform

runs when middleware is attached

creates the middleware wrapper

setup layer
Enter fullscreen mode Exit fullscreen mode

Service

runs on every request

executes middleware logic

runtime layer
Enter fullscreen mode Exit fullscreen mode

Visual Flow

App Startup
   ↓
Transform runs
   ↓
Middleware service created
   ↓
-----------------------------
Each Client Request
   ↓
Service runs
   ↓
Check request
   ↓
Allow or block
Enter fullscreen mode Exit fullscreen mode
  1. 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");
}
Enter fullscreen mode Exit fullscreen mode

Output

Checking entry for Ashwani
Checking entry for Ravi
Enter fullscreen mode Exit fullscreen mode

Explanation

GateFactory = like Transform

Gate = like Service
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

If present:

allow request to continue
Enter fullscreen mode Exit fullscreen mode

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())
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

Step-by-Step Theory of the Code

Part 1: Middleware marker struct

pub struct AuthMiddleware;
Enter fullscreen mode Exit fullscreen mode

This is the middleware type you attach with:

.wrap(AuthMiddleware)
Enter fullscreen mode Exit fullscreen mode

This struct does not contain logic itself.
It simply represents the middleware.

Part 2: Transform implementation

impl<S, B> Transform<S, ServiceRequest> for AuthMiddleware
Enter fullscreen mode Exit fullscreen mode

This means:

AuthMiddleware can transform an inner service into a middleware-wrapped service.

Here:


S = inner service

B = body type
Enter fullscreen mode Exit fullscreen mode

Part 3: new_transform()

fn new_transform(&self, service: S) -> Self::Future {
    ready(Ok(AuthMiddlewareService {
        service: Rc::new(service),
    }))
}
Enter fullscreen mode Exit fullscreen mode

This is the most important Transform function.

What it does

receives the inner service

wraps it inside AuthMiddlewareService

returns it
Enter fullscreen mode Exit fullscreen mode

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>,
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

If token exists:

middleware forwards request to inner service

handler runs normally

This is handler-generated response.

So it uses:

map_into_left_body()
Enter fullscreen mode Exit fullscreen mode

Simple Understanding of Left and Right Body

In middleware there can be two response sources:

Left body

Normal handler response
Enter fullscreen mode Exit fullscreen mode

Right body

Middleware custom response

So remember:

map_into_left_body() = request allowed

map_into_right_body() = request blocked by middleware
Enter fullscreen mode Exit fullscreen mode
  1. 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
}
Enter fullscreen mode Exit fullscreen mode

What Happens at Runtime

App startup

When application starts:

.wrap(AuthMiddleware)
Enter fullscreen mode Exit fullscreen mode

Actix triggers Transform.

So:

Transform creates AuthMiddlewareService
Enter fullscreen mode Exit fullscreen mode

middleware pipeline is prepared

Request 1: No token

User sends request without header:

GET /api/dashboard
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Middleware Service runs:

checks header

token exists

forwards request to handler

Enter fullscreen mode Exit fullscreen mode

Handler returns:

Welcome to dashboard

  1. Output Summary
Without token
Status: 401 Unauthorized
Body: Missing auth token
With token
Status: 200 OK
Body: Welcome to dashboard
Enter fullscreen mode Exit fullscreen mode
  1. Why Beginners Get Confused

Many beginners think middleware is only one thing.

They expect:

one struct

one trait

one request handler
Enter fullscreen mode Exit fullscreen mode

But Actix separates middleware into two levels:

Level 1: Creation

Handled by Transform
Enter fullscreen mode Exit fullscreen mode

Level 2: Execution

Handled by Service
Enter fullscreen mode Exit fullscreen mode

This design gives Actix great flexibility.

  1. Interview-Style Answer

If someone asks in interview:

What is the difference between Transform and Service in Actix middleware?
Enter fullscreen mode Exit fullscreen mode

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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. Which HTTP response is commonly returned when authentication fails?

A. 200 OK
B. 401 Unauthorized
C. 201 Created
D. 302 Redirect

✅ Answer: B

  1. 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.

  1. 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

  1. 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.

  1. 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

  1. Which trait must response body type implement in Actix middleware?

A. Clone
B. Serialize
C. MessageBody
D. Future

✅ Answer: C

  1. 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

  1. 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.

  1. 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

  1. Which Actix function allows attaching middleware to an application?

A. attach()
B. wrap()
C. register()
D. bind()

✅ Answer: B
App::wrap() attaches middleware.

  1. Which type is commonly used to represent asynchronous middleware results?

A. Result
B. LocalBoxFuture
C. AsyncResponse
D. FutureResult

✅ Answer: B

  1. 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

  1. 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

  1. Which middleware logic is typically executed before the handler?

A. Response serialization
B. Authentication check
C. Database backup
D. File upload

✅ Answer: B

  1. 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)