Feather Web Framework

repository·master·Indexed 21 days ago

https://github.com/bersisse/feather

A minimal, DX-first HTTP framework for Rust inspired by Express.js. It features a custom multithreaded runtime (Feather-Runtime) using a coroutine-based model via the may crate, allowing developers to write synchronous-looking code without async/await complexity. Key features include a Context API for state management, built-in JWT authentication, and support for both synchronous and asynchronous middleware via #[middleware_fn] and #[async_middleware] macros.

Tokens
26.7K
Snippets
107
Records
124
Agent score
72%

What's inside feather

  1. What is a Middleware in Feather?

    master

    A middleware is a component in Feather's request processing pipeline that processes HTTP requests and responses. It can inspect request data (headers, body, path), mutate the response, control the flow of execution, or access application state.

    Middlewares are defined by the Middleware trait, which requires a handle method:

    pub trait Middleware: Send + Sync {
        fn handle(&self, request: &mut Request, response: &mut Response, ctx: &AppContext) -> Outcome;
    }

    Control flow is managed via the Outcome type, which is a Result wrapping a MiddlewareResult. This allows using the ? operator for clean error propagation.

    pub enum MiddlewareResult {
        Next,           // Continue to next middleware
        NextRoute,      // Skip to next route handler
        End,            // Stop Executing and Send the Request.
    }
    pub type Outcome = Result<MiddlewareResult, Box<dyn Error>>;
  2. What is Feather Runtime and how does it work?

    master

    Feather Runtime is a high-concurrency web server engine designed to provide a synchronous-feeling development experience without the complexity of async/await or lifetime management.

    It achieves high performance through a coroutine-based model using the may crate. Instead of a thread-per-connection model, every incoming connection is assigned its own lightweight coroutine (green thread). Sockets are non-blocking and event-driven: when a coroutine encounters I/O that would normally block, it yields control back to the runtime, allowing other coroutines to execute. This allows the server to handle thousands of concurrent connections while letting developers write standard, synchronous-looking Rust code.

  3. What is AppContext? and how to create state

    master

    Feather uses AppContext as a type-safe, thread-safe container for application-wide state. This state is available to every request and lives for the entire duration of the application (until app.listen() is called).

    Read-Only State

    For data that does not need to change (like configuration), you can store it directly. Feather provides the data via an Arc pointer internally.

    Mutable State

    For data that requires modification (like counters, metrics, or database pools), you must wrap the type in the State<T> struct. State<T> uses parking_lot::Mutex to ensure thread-safe access.

    use feather::{App, AppContext, State};
    
    #[derive(Clone)]
    struct AppConfig { 
        database_url: String 
    }
    
    #[derive(Clone)]
    struct Counter { 
        count: i32 
    }
    
    fn main() {
        let mut app = App::new();
        
        // Read-only state (stored directly)
        app.context().set_state(AppConfig { 
            database_url: "postgresql://localhost/db".to_string() 
        });
    
        // Mutable state (wrapped in State<T>)
        app.context().set_state(State::new(Counter { count: 0 }));
    }
  4. Use wildcard routes for pattern matching

    master

    You can use the asterisk (*) wildcard to match any path structure. This is useful for prefix matching (e.g., /api/*) or creating catch-all routes (e.g., /*) for handling 404 errors.

    // Match any path starting with /api/
    app.get("/api/*", middleware!(|_req, res, _ctx| {
        res.send_text("API route");
        next!()
    }));
    
    // Catch-all route
    app.get("/*", middleware!(|_req, res, _ctx| {
        res.set_status(404);
        res.send_text("Not found");
        next!()
    }));
  5. Manage application state using the Context API

    master

    Feather provides a Context API to manage application-wide state (like database connections or shared counters) without needing complex extractors or macros.

    To use it:

    1. Wrap your state in a thread-safe container (like State<T>).
    2. Register the state in the app context using app.context().set_state(State::new(your_data)).
    3. Retrieve the state in a handler using ctx.get_state::<State<T>>().unwrap().

    Note: Because Feather is multithreaded, shared state should be wrapped in synchronization primitives (e.g., lock()) if it is mutable.

    use feather::{App, middleware_fn, next};
    
    #[derive(Debug)]
    struct Counter { pub count: i32 }
    
    #[middleware_fn]
    fn count() -> feather::Outcome {
        let counter = ctx.get_state::<State<Counter>>().unwrap();
        counter.lock().count += 1;
        res.send_text(format!("Counted! {}", counter.count));
        next!()
    }
    
    fn main() {
        let mut app = App::new();
        app.context().set_state(State::new(Counter { count: 0 }));
        app.get("/", count);
        app.listen("127.0.0.1:5050");
    }
  6. Define Custom JWT Claims with `#[derive(Claim)]`

    master

    The #[derive(Claim)] macro is the recommended way to define custom claims. It automatically implements the Claim trait and provides validation for specific attributes:

    • #[required]: Ensures the field is not empty.
    • #[exp]: Ensures the field is a valid Unix timestamp in the future.

    Example structure:

    #[derive(Serialize, Deserialize, Claim, Clone)]
    pub struct UserClaims {
        #[required]
        pub sub: String,        // Subject (usually user ID)
        #[required]
        pub email: String,      // Custom field
        pub role: String,       // Custom field
        #[exp]
        pub exp: usize,         // Expiration time (automatically validated)
    }
  7. How the middleware pattern works in Feather

    master

    Every route handler in Feather is a middleware. You define them using the middleware! macro, which provides three parameters to your closure:

    1. req: &mut Request - The incoming HTTP request containing headers, body, and metadata.
    2. res: &mut Response - The HTTP response object used to build the response.
    3. ctx: &AppContext - The application context for accessing shared state.

    Control Flow Macros

    • next!(): Continues execution to the next middleware in the chain.
    • next_route!(): (v0.8.0+) Skips the current route entirely, useful for logic-based routing.
    • end!(): (v0.8.0+) Stops all execution and sends the response immediately.
    middleware!(|req, res, ctx| {
        // Process the request
        // Modify the response
        next!() // Continue to next middleware or finish
    })
  8. Use Global Middleware vs Route-specific Middleware

    master

    Feather distinguishes between middleware applied to all requests and middleware applied to specific routes.

    • Global Middleware: Use app.use_middleware(middleware!(...)) to run logic on every incoming request (e.g., logging).
    • Route-specific Middleware: Defined directly within route methods like app.get("/path", middleware!(...)).
    // Global middleware
    app.use_middleware(middleware!(|req, res, _ctx| {
        println!("Request to: {}", req.uri);
        next!()
    }));
    
    // Route-specific middleware
    app.get("/", middleware!(|_req, res, _ctx| {
        res.send_text("Home page");
        next!()
    }));
  9. Default Error Handling in Feather

    master

    By default, Feather catches all unhandled errors and returns a 500 Internal Server Error response to the client. The error is logged to stderr. This occurs when an error is returned from a middleware using the ? operator or similar mechanisms.

    use feather::App;
    
    fn main() {
        let mut app = App::new();
        
        // If any error occurs, Feather returns 500
        app.get("/", middleware!(|_req, res, _ctx| {
            std::fs::File::open("non-existingfile.txt")?; // This will send a 500 response to the client
            next!()
        }));
        
        app.listen("127.0.0.1:5050");
    }
  10. Control flow with `next!()`, `next_route!()`, and `end!()`

    master

    Feather uses explicit macros to control the execution of the middleware chain:

    • next!(): The standard way to move to the next middleware in the current stack.
    • next_route!(): (New in 0.8.0) Tells the engine to abandon the current route matching entirely and look for the next path that matches the request.
    • end!(): Stops the chain immediately. Use this if you have already sent a response and want to prevent subsequent middleware (like loggers) from executing logic.
  11. Implement Middleware in Feather

    master

    Middleware is the core of Feather and can be implemented in three ways:

    1. Function-based: Use the #[middleware_fn] macro on a function. Use next!() to pass control to the next handler.
    2. Global Middleware: Register functions globally using app.use_middleware(handler).
    3. Struct-based: Implement the Middleware trait for a struct. This is useful for middleware that needs to hold internal state.

    When using #[middleware_fn], you have access to res (Response), req (Request), and ctx (AppContext).

    use feather::{middleware_fn, Request, Response, AppContext, Middleware, next, info};
    
    struct CustomMiddleware(String);
    
    impl Middleware for CustomMiddleware {
        fn handle(&self, _request: &mut Request, _response: &mut Response, _ctx: &AppContext) -> feather::Outcome {
            info!("Hii I am a Struct Middleware and this is my data: {}", self.0);
            next!()
        }
    }
  12. Apply Global vs Route-specific Middleware

    master

    Middleware execution order follows this hierarchy:

    1. Global middleware: Applied via app.use_middleware(), runs on every request in the order defined.
    2. Route-specific middleware: Applied directly to a route or router, runs after global middleware.

    Example of applying both:

    // Global middleware
    app.use_middleware(middleware!(|req, res, _ctx| {
        println!("Request: {} {}", req.method, req.uri);
        next!()
    }));
    
    // Route-specific middleware
    app.get("/", middleware!(|_req, res, _ctx| {
        res.send_text("Hello");
        next!()
    }));