Skip to main content

http

Attribute Macro http 

Source
#[http]
Expand description

Generate HTTP handlers from an impl block.

§Basic Usage

use server_less::http;

#[http]
impl UserService {
    async fn create_user(&self, name: String) -> User { /* ... */ }
}

§With URL Prefix

#[http(prefix = "/api/v1")]
impl UserService {
    // POST /api/v1/users
    async fn create_user(&self, name: String) -> User { /* ... */ }
}

§Per-Method Route Overrides

#[http]
impl UserService {
    // Override HTTP method: GET /data becomes POST /data
    #[route(method = "POST")]
    async fn get_data(&self, payload: String) -> String { /* ... */ }

    // Override path: POST /users becomes POST /custom-endpoint
    #[route(path = "/custom-endpoint")]
    async fn create_user(&self, name: String) -> User { /* ... */ }

    // Override both
    #[route(method = "PUT", path = "/special/{id}")]
    async fn do_something(&self, id: String) -> String { /* ... */ }

    // Skip route generation (internal methods)
    #[route(skip)]
    fn internal_helper(&self) -> String { /* ... */ }

    // Hide from OpenAPI but still generate route
    #[route(hidden)]
    fn secret_endpoint(&self) -> String { /* ... */ }
}

§Parameter Handling

#[http]
impl BlogService {
    // Path parameters (id, post_id, etc. go in URL)
    async fn get_post(&self, post_id: u32) -> Post { /* ... */ }
    // GET /posts/{post_id}

    // Query parameters (GET methods use query string)
    async fn search_posts(&self, query: String, tag: Option<String>) -> Vec<Post> {
        /* ... */
    }
    // GET /posts?query=rust&tag=tutorial

    // Body parameters (POST/PUT/PATCH use JSON body)
    async fn create_post(&self, title: String, content: String) -> Post {
        /* ... */
    }
    // POST /posts with body: {"title": "...", "content": "..."}
}

§Error Handling

#[http]
impl UserService {
    // Return Result for error handling
    async fn get_user(&self, id: u32) -> Result<User, MyError> {
        if id == 0 {
            return Err(MyError::InvalidId);
        }
        Ok(User { id, name: "Alice".into() })
    }

    // Return Option - None becomes 404
    async fn find_user(&self, email: String) -> Option<User> {
        // Returns 200 with user or 404 if None
        None
    }
}

§Server-Sent Events (SSE) Streaming

Return impl Stream<Item = T> to enable Server-Sent Events streaming.

Important for Rust 2024: You must add + use<> to impl Trait return types to explicitly capture all generic parameters in scope. This is required by the Rust 2024 edition’s stricter lifetime capture rules.

use futures::stream::{self, Stream};

#[http]
impl DataService {
    // Simple stream - emits values immediately
    // Note the `+ use<>` syntax for Rust 2024
    fn stream_numbers(&self, count: u32) -> impl Stream<Item = u32> + use<> {
        stream::iter(0..count)
    }

    // Async stream with delays
    async fn stream_events(&self, n: u32) -> impl Stream<Item = Event> + use<> {
        stream::unfold(0, move |count| async move {
            if count >= n {
                return None;
            }
            tokio::time::sleep(Duration::from_secs(1)).await;
            Some((Event { id: count }, count + 1))
        })
    }
}

Clients receive data as SSE:

data: {"id": 0}

data: {"id": 1}

data: {"id": 2}

Why + use<>?

  • Rust 2024 requires explicit capture of generic parameters in return position impl Trait
  • + use<> captures all type parameters and lifetimes from the function context
  • Without it, you’ll get compilation errors about uncaptured parameters
  • See: examples/streaming_service.rs for a complete working example

§Real-World Example

#[http(prefix = "/api/v1")]
impl UserService {
    // GET /api/v1/users?page=0&limit=10
    async fn list_users(
        &self,
        #[param(default = 0)] page: u32,
        #[param(default = 20)] limit: u32,
    ) -> Vec<User> {
        /* ... */
    }

    // GET /api/v1/users/{user_id}
    async fn get_user(&self, user_id: u32) -> Result<User, ApiError> {
        /* ... */
    }

    // POST /api/v1/users with body: {"name": "...", "email": "..."}
    #[response(status = 201)]
    #[response(header = "Location", value = "/api/v1/users/{id}")]
    async fn create_user(&self, name: String, email: String) -> Result<User, ApiError> {
        /* ... */
    }

    // PUT /api/v1/users/{user_id}
    async fn update_user(
        &self,
        user_id: u32,
        name: Option<String>,
        email: Option<String>,
    ) -> Result<User, ApiError> {
        /* ... */
    }

    // DELETE /api/v1/users/{user_id}
    #[response(status = 204)]
    async fn delete_user(&self, user_id: u32) -> Result<(), ApiError> {
        /* ... */
    }
}

§Generated Methods

  • http_router() -> axum::Router - Complete router with all endpoints
  • http_routes() -> Vec<&'static str> - List of route paths
  • openapi_spec() -> serde_json::Value - OpenAPI 3.0 specification (unless openapi = false)

§OpenAPI Control

By default, #[http] generates both HTTP routes and OpenAPI specs. You can disable OpenAPI generation:

#[http(openapi = false)]  // No openapi_spec() method generated
impl MyService { /* ... */ }

For standalone OpenAPI generation without HTTP routing, see #[openapi].