#web-framework #hyper #framework #tokio #request-response #web #http

awpak-rs

A lightweight web framework for Rust, built on top of Tokio and Hyper

1 unstable release

new 0.0.1 Feb 27, 2025

#216 in #request-response

MIT license

110KB
2.5K SLoC

awpak-rs

awpak-rs is a lightweight web framework for Rust, built on top of tokio and hyper. It simplifies request handling, middleware processing, and response generation, using declarative macros for a more ergonomic development experience.

⚠️ This is an initial version of the project, and the API may change in future releases.

Features

  • Declarative Macros: Define routes using #[get], #[post], #[put], #[delete], and more.
  • Flexible Request Handling: Extract query parameters, request headers, cookies, request bodies, and file uploads effortlessly.
  • Middleware Support: Define and chain middleware to modify requests and responses.
  • Context Sharing: Use middleware to pass shared state across request processing.
  • Automatic JSON Serialization: Responses and request bodies seamlessly integrate with serde.
  • File Upload Support: Handle multipart/form-data file uploads efficiently.

Installation

To use awpak-rs, add it to your Cargo.toml:

[dependencies]
awpak-rs = "0.0.1"
serde = { version = "1.0", features = ["derive"] }

Getting Started

Basic Example

use awpak_rs::*;
use serde::{Serialize, Deserialize};

#[awpak_main(ip = "127.0.0.1", port = "3001")]
fn main() {}

#[derive(Serialize, Deserialize, FromValue)]
struct Point {
    x: Option<f32>,
    y: f32,
}

#[get(url = "/get_point")]
fn get_point(#[query_params] point: Point) -> Point {
    point
}

Run the server and test with:

curl "http://127.0.0.1:3001/get_point?x=1&y=2"

Response:

{"x":1.0,"y":2.0}

Setting Up the Main Function

The #[awpak_main] macro defines the entry point of the application:

#[awpak_main(ip = "127.0.0.1", port = "3001")]
fn main() {}
  • ip (optional): The IP address to bind to (default: 127.0.0.1).
  • port (optional): The port the server will listen on (default: 3000).

Defining Routes

Endpoints are defined using route macros such as #[get] and #[post]:

#[get(url = "/hello")]
fn hello() -> &'static str {
    "Hello, world!"
}

Extracting Query Parameters

#[get(url = "/greet")]
fn greet(
    #[query_param] name: String
) -> String {
    format!("Hello, {}!", name)
}

Path Variables

#[get(url = "/user/{id}")]
fn get_user(
    #[path_variable] id: usize
) -> String {
    format!("User ID: {}", id)
}

Extracting a User from a Path Variable

#[get(url = "/user/{id}")]
async fn get_user(#[path_variable] user: User) -> User {
    user
}

impl FromAsyncStr<User> for User {
    async fn from_async_str(io: &IO, s: &str) -> Result<User, ()> {
        let user = get_user_from_db(s).await;
        Ok(user)
    }
}

Handling Request and Response Headers

Extract request headers:

#[post(url = "/header-example")]
fn header_example(
    #[request_headers] headers: Headers
) -> String {
    match headers.get_value("content-type") {
        Some(content_type) => content_type,
        None => "Content-Type not found".to_string(),
    }
}

Modify response headers:

#[post(url = "/set-header")]
fn set_header(
    #[response_headers] mut headers: Headers
) -> String {
    headers.replace_header("content-type", "application/json");
    "Header set".to_string()
}

Working with Cookies

Retrieve request cookies:

#[get(url = "/get-cookie")]
fn get_cookie(
    #[request_cookies] cookies: Cookies
) -> String {
    match cookies.find_first_by_name("session_id") {
        Some(cookie) => format!("Session ID: {}", cookie.value()),
        None => "No session cookie found".to_string(),
    }
}

Modify response cookies:

#[post(url = "/set-cookie")]
fn set_cookie(
    #[response_cookies] mut res_cookies: Cookies
) -> String {
    res_cookies.add_cookie("user_id=12345; Path=/;").unwrap();
    "Cookie set successfully".to_string()
}

Middleware Support

Middlewares allow modifying incoming requests, responses, or setting shared context before or after executing an endpoint. They can:

  • Modify request headers, cookies, or body.
  • Modify response headers, cookies, or status codes.
  • Add authentication or logging logic.
  • Execute conditionally based on URL patterns, HTTP methods, or execution order.
#[middleware]
fn log_requests(mut io: IO) -> MiddlewareResponse {
    println!("Received request: {} {}", io.request.method, io.request.uri.path);
    MiddlewareResponse::Next(io)
}

Middlewares return:

  • MiddlewareResponse::Next(io): Continue to the next middleware or endpoint.
  • MiddlewareResponse::Cancel(io): Stop execution and return a response immediately.

Using Context in Middleware

Middleware can set shared context using IO::set_context:

struct ContextExample {
    value: usize,
}

#[middleware]
fn set_context(mut io: IO) -> MiddlewareResponse {
    io.set_context(ContextExample { value: 42 });
    MiddlewareResponse::Next(io)
}

Endpoints can then retrieve this context:

#[get(url = "/context")]
fn get_context(
    #[context] ctx: Option<&ContextExample>
) -> String {
    match ctx {
        Some(c) => format!("Context value: {}", c.value),
        None => "No context available".to_string(),
    }
}

Handling File Uploads

Extract a single uploaded file:

#[post(url = "/upload")]
fn upload_file(
    #[part_file] file: FileData
) -> usize {
    file.bytes.len()
}

Extract multiple uploaded files:

#[post(url = "/upload-multiple")]
fn upload_multiple_files(
    #[part_files] files: Vec<FileData>
) -> usize {
    files.iter().map(|f| f.bytes.len()).sum()
}

Supported HTTP Methods

Awpak-rs supports the following HTTP methods:

  • #[get]
  • #[post]
  • #[put]
  • #[delete]
  • #[patch]
  • #[head]
  • #[options]
  • #[trace]
  • #[connect]

License

Awpak-rs is released under the MIT License.

Dependencies

~12–20MB
~341K SLoC