What is the difference between axum::routing::get and axum::routing::get_service for route handlers?

axum::routing::get accepts handler functions that use Axum's extractor system, automatically deserializing request components into function parameters. axum::routing::get_service accepts any type implementing tower::Service, providing lower-level control over request handling without the extractor convenience. Use get for typical Axum handlers where you want automatic parameter extraction, JSON parsing, and response serialization. Use get_service when integrating existing Tower services, implementing middleware-like behavior, or when you need direct access to the raw Request and Response types without extractor magic.

Basic get Handler

use axum::{
    routing::get,
    Router,
    extract::{Path, Query, Json},
    response::IntoResponse,
};
use serde::{Deserialize, Serialize};
 
#[derive(Deserialize)]
struct FilterParams {
    category: Option<String>,
    limit: Option<usize>,
}
 
#[derive(Serialize)]
struct Item {
    id: u32,
    name: String,
}
 
async fn list_items(
    Path(user_id): Path<u32>,
    Query(params): Query<FilterParams>,
) -> impl IntoResponse {
    let category = params.category.as_deref().unwrap_or("all");
    let limit = params.limit.unwrap_or(10);
    
    Json(vec![
        Item { id: 1, name: format!("Item for user {} in {}", user_id, category) },
    ])
}
 
fn app() -> Router {
    Router::new()
        .route("/users/:user_id/items", get(list_items))
}

get accepts handler functions with extractors as parameters. Axum automatically extracts Path, Query, and other components.

Basic get_service Handler

use axum::{
    routing::get_service,
    Router,
};
use tower::Service;
use std::task::{Context, Poll};
use http::{Request, Response};
use hyper::body::Body;
 
// A simple service that handles requests directly
struct MyService;
 
impl Service<Request<Body>> for MyService {
    type Response = Response<Body>;
    type Error = Box<dyn std::error::Error + Send + Sync>;
    type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
 
    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        Poll::Ready(Ok(()))
    }
 
    fn call(&mut self, req: Request<Body>) -> Self::Future {
        // Direct access to raw Request - no extractors
        let path = req.uri().path();
        let response = Response::builder()
            .status(200)
            .body(Body::from(format!("Path: {}", path)))
            .unwrap();
        std::future::ready(Ok(response))
    }
}
 
fn app() -> Router {
    Router::new()
        .route("/raw", get_service(MyService))
}

get_service accepts any Service implementation. You work with raw Request and Response.

Extractor System with get

use axum::{
    extract::{Extension, Path, Json, Query, State},
    response::IntoResponse,
    Router,
    routing::get,
};
use serde::{Deserialize, Serialize};
 
#[derive(Clone)]
struct AppState {
    db: String,  // Simplified for example
}
 
#[derive(Deserialize)]
struct SearchParams {
    q: String,
    page: Option<u32>,
}
 
#[derive(Serialize)]
struct SearchResult {
    query: String,
    results: Vec<String>,
}
 
async fn search(
    State(state): State<AppState>,
    Extension(user_id): Extension<u32>,
    Query(params): Query<SearchParams>,
) -> impl IntoResponse {
    // All extractors are automatically resolved
    // State is injected from the router
    // Extension is set via middleware
    // Query is parsed from URL parameters
    
    Json(SearchResult {
        query: params.q,
        results: vec![format!("Result from DB: {}", state.db)],
    })
}
 
fn app() -> Router {
    Router::new()
        .route("/search", get(search))
        .with_state(AppState { db: "mydb".to_string() })
}

get handlers can use multiple extractors. Axum handles extraction automatically.

Manual Request Handling with get_service

use axum::{Router, routing::get_service};
use tower::Service;
use http::{Request, Response, Method};
use hyper::body::Body;
use std::task::{Context, Poll};
 
struct ApiHandler;
 
impl Service<Request<Body>> for ApiHandler {
    type Response = Response<Body>;
    type Error = Box<dyn std::error::Error + Send + Sync>;
    type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
 
    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        Poll::Ready(Ok(()))
    }
 
    fn call(&mut self, req: Request<Body>) -> Self::Future {
        // Must manually extract everything from the request
        let path = req.uri().path().to_string();
        let method = req.method().clone();
        let headers = req.headers().clone();
        
        // Manual routing based on method/path
        let response = match (method, path.as_str()) {
            (Method::GET, "/api/status") => {
                Response::builder()
                    .status(200)
                    .body(Body::from("{\"status\":\"ok\"}"))
                    .unwrap()
            }
            (Method::POST, "/api/data") => {
                // Would need to manually read body here
                Response::builder()
                    .status(201)
                    .body(Body::from("Created"))
                    .unwrap()
            }
            _ => {
                Response::builder()
                    .status(404)
                    .body(Body::from("Not Found"))
                    .unwrap()
            }
        };
        
        std::future::ready(Ok(response))
    }
}
 
fn app() -> Router {
    Router::new()
        .route("/api/*path", get_service(ApiHandler))
}

With get_service, you handle routing, parsing, and response building manually.

Using Existing Tower Services

use axum::{Router, routing::get_service};
use tower::ServiceBuilder;
use tower_http::{services::ServeFile, ServiceBuilderExt};
use std::path::PathBuf;
 
fn app() -> Router {
    // Serve static files using an existing Tower service
    Router::new()
        // ServeFile is a Tower service that handles file serving
        .route("/static/index.html", get_service(ServeFile::new("public/index.html")))
        .route("/static/style.css", get_service(ServeFile::new("public/style.css")))
}
 
// Another example: using tower-http services
use tower_http::services::ServeDir;
 
fn app_with_serve_dir() -> Router {
    Router::new()
        // ServeDir handles all requests under /assets
        .nest_service("/assets", get_service(ServeDir::new("public/assets")))
}

get_service integrates existing Tower services like ServeFile and ServeDir.

Service with Middleware

use axum::{Router, routing::get_service};
use tower::ServiceBuilder;
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
use http::{Request, Response};
use hyper::body::Body;
 
struct MyApiService;
 
impl tower::Service<Request<Body>> for MyApiService {
    type Response = Response<Body>;
    type Error = Box<dyn std::error::Error + Send + Sync>;
    type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
 
    fn poll_ready(&mut self, _cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
        std::task::Poll::Ready(Ok(()))
    }
 
    fn call(&mut self, req: Request<Body>) -> Self::Future {
        let response = Response::builder()
            .status(200)
            .body(Body::from("API response"))
            .unwrap();
        std::future::ready(Ok(response))
    }
}
 
fn app() -> Router {
    // Wrap service with Tower middleware
    let service = ServiceBuilder::new()
        .layer(TraceLayer::new_for_http())
        .layer(CompressionLayer::new())
        .service(MyApiService);
 
    Router::new()
        .route("/api", get_service(service))
}

get_service works naturally with Tower middleware layers.

Combining get and get_service

use axum::{
    Router,
    routing::{get, get_service},
    extract::Json,
    response::IntoResponse,
};
use tower_http::services::ServeDir;
 
// Regular handler using extractors
async fn api_handler() -> impl IntoResponse {
    Json(serde_json::json!({ "status": "ok" }))
}
 
fn app() -> Router {
    Router::new()
        // Use get for API endpoints with extractors
        .route("/api/status", get(api_handler))
        
        // Use get_service for static file serving
        .nest_service("/static", get_service(ServeDir::new("public/static")))
        
        // Use get_service for existing services
        .route("/metrics", get_service(prometheus_service()))
}
 
fn prometheus_service() -> impl tower::Service<http::Request<hyper::body::Body>, Response = http::Response<hyper::body::Body>, Error = std::convert::Infallible> {
    // A service that returns Prometheus metrics
    tower::service_fn(|_req: http::Request<hyper::body::Body>| async {
        Ok::<_, std::convert::Infallible>(
            http::Response::builder()
                .status(200)
                .body(hyper::body::Body::from("# HELP requests_total Total requests\nrequests_total 42\n"))
                .unwrap()
        )
    })
}

Mix get and get_service based on what each endpoint needs.

Service Using service_fn

use axum::{Router, routing::get_service};
use tower::service_fn;
use http::{Request, Response};
use hyper::body::Body;
 
fn app() -> Router {
    Router::new()
        .route("/health", get_service(service_fn(|_req: Request<Body>| async {
            // service_fn creates a Service from a closure
            Ok::<_, Box<dyn std::error::Error + Send + Sync>>(
                Response::builder()
                    .status(200)
                    .body(Body::from("{\"healthy\":true}"))
                    .unwrap()
            )
        })))
}

service_fn creates a service from an async closure, useful for simple get_service handlers.

Error Handling Differences

use axum::{
    routing::{get, get_service},
    Router,
    extract::Json,
    response::{IntoResponse, Response},
};
use tower::Service;
use http::Request;
use hyper::body::Body;
use serde::Serialize;
 
#[derive(Serialize)]
struct ErrorResponse {
    error: String,
}
 
// get handler: use Result with IntoResponse
async fn handler_with_error() -> Result<Json<String>, impl IntoResponse> {
    // Axum's IntoResponse for Result handles errors
    Err((http::StatusCode::BAD_REQUEST, "Invalid request"))
}
 
// get_service: handle errors in the Service impl
struct ErrorService;
 
impl Service<Request<Body>> for ErrorService {
    type Response = Response<Body>;
    type Error = Box<dyn std::error::Error + Send + Sync>;
    type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
 
    fn poll_ready(&mut self, _cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
        std::task::Poll::Ready(Ok(()))
    }
 
    fn call(&mut self, _req: Request<Body>) -> Self::Future {
        // Must build error response manually
        let response = Response::builder()
            .status(400)
            .body(Body::from("{\"error\":\"Bad request\"}"))
            .unwrap();
        std::future::ready(Ok(response))
    }
}

get handlers use Rust's Result with automatic error conversion. get_service requires manual error handling.

State Access Differences

use axum::{
    routing::{get, get_service},
    Router,
    extract::State,
};
use tower::Service;
use http::{Request, Response};
use hyper::body::Body;
use std::sync::Arc;
 
#[derive(Clone)]
struct AppState {
    data: String,
}
 
// get: State extractor provides easy access
async fn get_handler(State(state): State<AppState>) -> String {
    state.data
}
 
// get_service: Must use request extensions or other mechanisms
struct ServiceWithState {
    // Services need to hold state directly
    state: AppState,
}
 
impl Service<Request<Body>> for ServiceWithState {
    type Response = Response<Body>;
    type Error = Box<dyn std::error::Error + Send + Sync>;
    type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
 
    fn poll_ready(&mut self, _cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
        std::task::Poll::Ready(Ok(()))
    }
 
    fn call(&mut self, req: Request<Body>) -> Self::Future {
        // State is available through self
        // Could also access through request extensions
        let data = self.state.data.clone();
        let response = Response::builder()
            .status(200)
            .body(Body::from(data))
            .unwrap();
        std::future::ready(Ok(response))
    }
}
 
fn app() -> Router {
    let state = AppState { data: "hello".to_string() };
 
    Router::new()
        .route("/extractor", get(get_handler))
        .route("/service", get_service(ServiceWithState { state: state.clone() }))
        .with_state(state)
}

get handlers use State extractor. get_service requires state in the service itself.

Performance Considerations

use axum::{
    routing::{get, get_service},
    Router,
};
use tower::Service;
 
// get: Overhead from extractor resolution
async fn extractors_overhead() {
    // Each extractor has resolution cost:
    // - Path parsing
    // - Query deserialization  
    // - JSON body parsing
    // - Extension lookups
    // These costs add up but are usually negligible
}
 
// get_service: Potentially lower overhead for simple handlers
struct MinimalService;
 
impl Service<http::Request<hyper::body::Body>> for MinimalService {
    type Response = http::Response<hyper::body::Body>;
    type Error = std::convert::Infallible;
    type Future = std::future::Ready<Result<Self::Response, Self::Error>>;
 
    fn poll_ready(&mut self, _cx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
        std::task::Poll::Ready(Ok(()))
    }
 
    fn call(&mut self, _req: http::Request<hyper::body::Body>) -> Self::Future {
        // Direct response without extractor overhead
        std::future::ready(Ok(http::Response::new(hyper::body::Body::from("OK"))))
    }
}
 
// In practice: use get for most handlers
// The convenience outweighs minimal overhead
// Use get_service when:
// - Integrating existing services
// - Need raw request access
// - Static file serving (tower-http services)

Performance differences are usually negligible. Choose based on convenience and integration needs.

When to Use Each

use axum::{
    routing::{get, get_service},
    Router,
    extract::{Json, Path},
};
use tower_http::services::ServeDir;
use tower::service_fn;
 
// Use get for: handlers that need extractors
async fn api_handler(
    Path(id): Path<u32>,
    Json(payload): Json<serde_json::Value>,
) -> Json<serde_json::Value> {
    Json(serde_json::json!({ "id": id, "received": payload }))
}
 
// Use get_service for: existing Tower services
fn setup_routes() -> Router {
    Router::new()
        // Handler with extractors -> get
        .route("/api/:id", get(api_handler))
        
        // Static file serving -> get_service
        .nest_service("/static", get_service(ServeDir::new("public")))
        
        // Health check with minimal overhead -> get_service
        .route("/health", get_service(service_fn(|_| async {
            Ok::<_, std::convert::Infallible>(
                http::Response::new(hyper::body::Body::from("OK"))
            )
        })))
        
        // Existing service -> get_service
        .route("/metrics", get_service(prometheus_exporter()))
}

Choose based on your needs: extractors favor get, integration favors get_service.

Summary

Aspect get get_service
Handler type Async function Tower Service
Extractors Automatic parameter extraction Manual request parsing
State access State extractor Service struct fields or extensions
Error handling Result with IntoResponse Manual response building
Integration Axum-specific Any Tower service
Static files Possible but verbose Use ServeDir, ServeFile
Request access Through extractors Raw Request<Body>

Synthesis

axum::routing::get and axum::routing::get_service serve different purposes in route handling:

get is for Axum-native handlers. It accepts async functions where parameters are automatically extracted using Axum's extractor system. Use get when:

  • Writing new handlers that benefit from automatic extraction
  • Parsing JSON, query params, path segments
  • Injecting state via State extractor
  • Using middleware that operates on extracted data
  • Building typical CRUD APIs

get_service is for Tower service integration. It accepts any Service<Request> implementation, giving raw access to the HTTP request and response. Use get_service when:

  • Integrating existing Tower services (ServeDir, ServeFile, etc.)
  • Wrapping services with Tower middleware
  • Building low-level handlers that need raw request access
  • Creating services that don't fit the extractor model
  • Performance-critical paths where extractor overhead matters (rare)

Key insight: The choice isn't about capability—both can handle any HTTP request. The difference is ergonomics vs. control. get provides excellent ergonomics through extractors, making common patterns trivial. get_service provides low-level control for integrating with the broader Tower ecosystem. Most applications use get for route handlers and get_service for static files, metrics endpoints, or integrating pre-existing services.