Scopes and Nested Routes
Group related routes together.
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
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:
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:
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():
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/v1so you can introduce/api/v2without breaking clients. - A scope with an empty route path (
"") handles the exact prefix, for exampleGET /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.
Apply middleware to one scope only.
Add .wrap(Logger::default()) to the /admin scope and call a route outside it.
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.
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.
Explanation: Put specific routes first and the broad fallback last.
Practical Exercise
Version and organize an API
TaskCreate 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.