Skip to main content
Lesson 1 of 2Intermediate⏱ 10 mins

Scopes and Nested Routes

Group related routes together.

0% of path

Learning Objectives

  • Use web::scope to group routes

##Grouping routes under a prefix

As an API grows, routes pile up: users, posts, admin tools, health checks. A scope groups related routes under a shared path prefix, and lets you share middleware and configuration for the whole group.

#Your first scope

code_blocksrust
use actix_web::{get, web, App, HttpServer, Responder};

#[get("/users")]
async fn list_users() -> impl Responder { "users" }

#[get("/users/{id}")]
async fn get_user(id: web::Path<u32>) -> impl Responder {
    format!("user {}", id.into_inner())
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().service(
            web::scope("/api/v1")
                .service(list_users)
                .service(get_user),
        )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

The handlers keep their own paths, and the scope adds /api/v1 in front. Your routes now answer /api/v1/users and /api/v1/users/{id}.

#Nesting scopes

Scopes can contain other scopes, which models a tree of URLs:

code_blocksrust
web::scope("/api")
    .service(
        web::scope("/v1")
            .service(web::scope("/users").route("", web::get().to(list_users_fn)))
            .service(web::scope("/posts")),
    )

#Middleware and data per scope

A scope can have its own middleware and its own app_data. This is the clean way to protect only the admin routes:

code_blocksrust
use actix_web::middleware::Logger;

web::scope("/admin")
    .wrap(Logger::default())      // only for /admin routes
    .route("/stats", web::get().to(stats))

#Organizing with configure

For larger projects, put each feature in its own function and attach it with .configure():

code_blocksrust
fn users_config(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::scope("/users")
            .route("", web::get().to(list_users_fn))
            .route("/{id}", web::get().to(get_user_fn)),
    );
}

App::new().service(web::scope("/api").configure(users_config))

Now each module owns its routes, and main.rs stays short.

#Tips

  • Keep versioned prefixes such as /api/v1 so you can introduce /api/v2 without breaking clients.
  • A scope with an empty route path ("") handles the exact prefix, for example GET /users.
  • Routes are matched in registration order, so register specific routes before catch-alls.

"What If?" Experiment

Learning by doing. Modify the code to see what happens.

Goal

Apply middleware to one scope only.

Starting Code
web::scope("/admin").route("/stats", web::get().to(stats))
Suggested Modification

Add .wrap(Logger::default()) to the /admin scope and call a route outside it.

Expected Result

Only /admin requests are logged.

Explanation: Middleware attached to a scope wraps only that scope's routes, which keeps unrelated endpoints unaffected.

Common Mistakes

Anticipate and avoid these frequent pitfalls when working with this concept.

!

Doubling the prefix in handlers

The route attribute already contains the scope prefix, so the final path has it twice.

Incorrect Pattern
#[get("/api/v1/users")] // inside web::scope("/api/v1")
Correct Pattern
#[get("/users")]

Explanation: The scope adds its prefix automatically. Write only the part that belongs to the handler.

!

Registering a catch-all before specific routes

Routes are checked in order, so the first match wins.

Incorrect Pattern
.service(web::resource("/{tail:.*}").to(not_found)).service(list_users)
Correct Pattern
.service(list_users).service(web::resource("/{tail:.*}").to(not_found))

Explanation: Put specific routes first and the broad fallback last.

Practical Exercise

Version and organize an API

Task

Create a /api/v1 scope containing a /health route and a /users scope with GET "".

Requirements

  • GET /api/v1/health returns "ok"
  • GET /api/v1/users returns "users"

Need a hint?

Try solving the exercise in your local environment first. Once you have a solution, or if you get completely stuck, reveal the answer.

Real-World Application

Large services mount one module per feature with configure(), version the whole API with a prefix and attach authentication middleware to the protected scope only.

Key Takeaways

  • web::scope("/prefix") groups routes under a shared path.
  • Scopes can nest and can carry their own middleware and data.
  • Use .configure() with a function per feature to keep main.rs small.
  • Version your API with a /api/v1 style prefix.

Finished reading?

Mark this lesson as done to track your progress and earn XP.