|
| 1 | +# Handlers & Extractors |
| 2 | + |
| 3 | +The **Handler** is the fundamental unit of work in RustAPI. It transforms an incoming HTTP request into an outgoing HTTP response. |
| 4 | + |
| 5 | +Unlike many web frameworks that enforce a strict method signature (e.g., `fn(req: Request, res: Response)`), RustAPI embraces a flexible, type-safe approach powered by Rust's trait system. |
| 6 | + |
| 7 | +## The Philosophy: "Ask for what you need" |
| 8 | + |
| 9 | +In RustAPI, you don't manually parse the request object inside your business logic. Instead, you declare the data you need as function arguments, and the framework's **Extractors** handle the plumbing for you. |
| 10 | + |
| 11 | +If the data cannot be extracted (e.g., missing header, invalid JSON), the request is rejected *before* your handler is ever called. This means your handler logic is guaranteed to operate on valid, type-safe data. |
| 12 | + |
| 13 | +## Anatomy of a Handler |
| 14 | + |
| 15 | +A handler is simply an asynchronous function that takes zero or more **Extractors** as arguments and returns something that implements `IntoResponse`. |
| 16 | + |
| 17 | +```rust |
| 18 | +use rustapi::prelude::*; |
| 19 | + |
| 20 | +async fn create_user( |
| 21 | + State(db): State<DbPool>, // 1. Dependency Injection |
| 22 | + Path(user_id): Path<Uuid>, // 2. URL Path Parameter |
| 23 | + Json(payload): Json<CreateUser>, // 3. JSON Request Body |
| 24 | +) -> Result<impl IntoResponse, ApiError> { |
| 25 | + |
| 26 | + let user = db.create_user(user_id, payload).await?; |
| 27 | + |
| 28 | + Ok((StatusCode::CREATED, Json(user))) |
| 29 | +} |
| 30 | +``` |
| 31 | + |
| 32 | +### Key Rules |
| 33 | +1. **Order Matters (Slightly)**: Extractors that consume the request body (like `Json<T>` or `Multipart`) must be the *last* argument. This is because the request body is a stream that can only be read once. |
| 34 | +2. **Async by Default**: Handlers are `async fn`. This allows non-blocking I/O operations (DB calls, external API requests). |
| 35 | +3. **Debuggable**: Handlers are just functions. You can unit test them easily. |
| 36 | + |
| 37 | +## Extractors: The `FromRequest` Trait |
| 38 | + |
| 39 | +Extractors are types that implement `FromRequest` (or `FromRequestParts` for headers/query params). They isolate the "HTTP parsing" logic from your "Business" logic. |
| 40 | + |
| 41 | +### Common Build-in Extractors |
| 42 | + |
| 43 | +| Extractor | Source | Example Usage | |
| 44 | +|-----------|--------|---------------| |
| 45 | +| `Path<T>` | URL Path Segments | `fn get_user(Path(id): Path<u32>)` | |
| 46 | +| `Query<T>` | Query String | `fn search(Query(params): Query<SearchFn>)` | |
| 47 | +| `Json<T>` | Request Body | `fn update(Json(data): Json<UpdateDto>)` | |
| 48 | +| `HeaderMap` | HTTP Headers | `fn headers(headers: HeaderMap)` | |
| 49 | +| `State<T>` | Application State | `fn db_op(State(pool): State<PgPool>)` | |
| 50 | +| `Extension<T>` | Request-local extensions | `fn logic(Extension(user): Extension<User>)` | |
| 51 | + |
| 52 | +### Custom Extractors |
| 53 | + |
| 54 | +You can create your own extractors to encapsulate repetitive validation or parsing logic. For example, extracting a user ID from a verified JWT: |
| 55 | + |
| 56 | +```rust |
| 57 | +pub struct AuthenticatedUser(pub Uuid); |
| 58 | + |
| 59 | +#[async_trait] |
| 60 | +impl<S> FromRequestParts<S> for AuthenticatedUser |
| 61 | +where |
| 62 | + S: Send + Sync, |
| 63 | +{ |
| 64 | + type Rejection = ApiError; |
| 65 | + |
| 66 | + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> { |
| 67 | + let auth_header = parts.headers.get("Authorization") |
| 68 | + .ok_or(ApiError::Unauthorized("Missing token"))?; |
| 69 | + |
| 70 | + let token = auth_header.to_str().map_err(|_| ApiError::Unauthorized("Invalid token"))?; |
| 71 | + let user_id = verify_jwt(token)?; // Your verification logic |
| 72 | + |
| 73 | + Ok(AuthenticatedUser(user_id)) |
| 74 | + } |
| 75 | +} |
| 76 | + |
| 77 | +// Usage in handler: cleaner and reusable! |
| 78 | +async fn profile(AuthenticatedUser(uid): AuthenticatedUser) -> impl IntoResponse { |
| 79 | + format!("User ID: {}", uid) |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +## Responses: The `IntoResponse` Trait |
| 84 | + |
| 85 | +A handler can return any type that implements `IntoResponse`. RustAPI provides implementations for many common types: |
| 86 | + |
| 87 | +- `StatusCode` (e.g., return `200 OK` or `404 Not Found`) |
| 88 | +- `Json<T>` (serializes struct to JSON) |
| 89 | +- `String` / `&str` (plain text response) |
| 90 | +- `Vec<u8>` / `Bytes` (binary data) |
| 91 | +- `HeaderMap` (response headers) |
| 92 | +- `Html<String>` (HTML content) |
| 93 | + |
| 94 | +### Tuple Responses |
| 95 | +You can combine types using tuples to set status codes and headers along with the body: |
| 96 | + |
| 97 | +```rust |
| 98 | +// Returns 201 Created + JSON Body |
| 99 | +async fn create() -> (StatusCode, Json<User>) { |
| 100 | + (StatusCode::CREATED, Json(user)) |
| 101 | +} |
| 102 | + |
| 103 | +// Returns Custom Header + Plain Text |
| 104 | +async fn custom() -> (HeaderMap, &'static str) { |
| 105 | + let mut headers = HeaderMap::new(); |
| 106 | + headers.insert("X-Custom", "Value".parse().unwrap()); |
| 107 | + (headers, "Response with headers") |
| 108 | +} |
| 109 | +``` |
| 110 | + |
| 111 | +### Error Handling |
| 112 | + |
| 113 | +Handlers often return `Result<T, E>`. If the handler returns `Ok(T)`, the `T` is converted to a response. If it returns `Err(E)`, the `E` is converted to a response. |
| 114 | + |
| 115 | +This effectively means your `Error` type must implement `IntoResponse`. |
| 116 | + |
| 117 | +```rust |
| 118 | +// Recommended pattern: Centralized API Error enum |
| 119 | +pub enum ApiError { |
| 120 | + NotFound(String), |
| 121 | + InternalServerError, |
| 122 | +} |
| 123 | + |
| 124 | +impl IntoResponse for ApiError { |
| 125 | + fn into_response(self) -> Response { |
| 126 | + let (status, message) = match self { |
| 127 | + ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg), |
| 128 | + ApiError::InternalServerError => (StatusCode::INTERNAL_SERVER_ERROR, "Something went wrong".to_string()), |
| 129 | + }; |
| 130 | + |
| 131 | + (status, Json(json!({ "error": message }))).into_response() |
| 132 | + } |
| 133 | +} |
| 134 | +``` |
| 135 | + |
| 136 | +## Best Practices |
| 137 | + |
| 138 | +1. **Keep Handlers Thin**: Move complex business logic to "Service" structs or domain modules. Handlers should focus on HTTP translation (decoding request -> calling service -> encoding response). |
| 139 | +2. **Use `State` for Dependencies**: Avoid global variables. Pass DB pools and config via `State`. |
| 140 | +3. **Parse Early**: Use specific types in `Json<T>` structs rather than `serde_json::Value` to leverage the type system for validation. |
0 commit comments