unwarp 2.0.0

A minimal, ergonomic wrapper around warp — define routes, attach handlers, and serve. No boilerplate, no filter chains.
Documentation

unwarp

A minimal, ergonomic wrapper around warp — define routes, attach handlers, and serve. No boilerplate, no filter chains.

Crates.io docs.rs GitHub License: MIT


Overview

unwarp sits on top of warp and hides the filter-composition ceremony behind a simple builder API:

  1. Pick an HTTP method and path with RouteBuilder.
  2. Optionally specify a JSON body or query-string type.
  3. Attach an async handler with .handle(...) — you get back a type-erased _Filter.
  4. Register every route on an Unwarp instance with .route(...).
  5. Call .serve(addr).await to start listening.

Installation

[dependencies]
unwarp = "2.0.0"

[dev-dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

unwarp re-exports warp internally. You do not need to add warp to your own Cargo.toml.


Quick-start

use unwarp::prelude::*;  // also brings `warp` module into scope

#[tokio::main]
async fn main() {
    let mut server = Unwarp::new();

    server.route(
        RouteBuilder::get("ping")
            .handle(|| async {
                Ok::<_, warp::Rejection>(warp::reply::html("pong"))
            })
    );

    server.serve(([127, 0, 0, 1], 3030)).await;
}

Route builders

Plain route (no body / no query params)

use unwarp::prelude::*;

RouteBuilder::get("hello")
    .handle(|| async {
        Ok::<_, warp::Rejection>(warp::reply::html("Hello, world!"))
    });

JSON body

Declare the expected body type with .json::<T>(). T must implement serde::DeserializeOwned.

use serde::Deserialize;
use unwarp::prelude::*;

#[derive(Deserialize)]
struct CreateUser { name: String, email: String }

RouteBuilder::post("users")
    .json::<CreateUser>()
    .handle(|body: CreateUser| async move {
        Ok::<_, warp::Rejection>(warp::reply::json(&body.name))
    });

Query parameters

Declare the query-string type with .query::<T>(). T must implement serde::DeserializeOwned.

use serde::Deserialize;
use unwarp::prelude::*;

#[derive(Deserialize)]
struct Search { q: String, limit: Option<u32> }

RouteBuilder::get("search")
    .query::<Search>()
    .handle(|params: Search| async move {
        Ok::<_, warp::Rejection>(warp::reply::json(&params.q))
    });

Supported HTTP methods

Builder constructor HTTP method
RouteBuilder::get(path) GET
RouteBuilder::post(path) POST
RouteBuilder::put(path) PUT
RouteBuilder::delete(path) DELETE
RouteBuilder::patch(path) PATCH
RouteBuilder::head(path) HEAD
RouteBuilder::options(path) OPTIONS

Constructing responses

Unwarp ships two static helpers so you rarely need to write Ok::<_, Rejection>(...) yourself.

Unwarp::with_status

RouteBuilder::delete("items/42")
    .handle(|| async {
        Unwarp::with_status(Status::NoContent, warp::reply())
    });

Unwarp::json

RouteBuilder::get("config")
    .handle(|| async {
        Unwarp::json(&serde_json::json!({ "version": "1.0" }))
    });

Unwarp::json_with_status

Serialises a value as JSON and sets the status code in one call. Useful for 201 Created, 202 Accepted, etc.

use serde::Serialize;
use unwarp::prelude::*;

#[derive(Serialize)]
struct Created { id: u64 }

RouteBuilder::post("items")
    .json::<()>()
    .handle(|_| async move {
        Unwarp::json_with_status(Status::Created, &Created { id: 42 })
    });

unwarp! macro (prelude)

The unwarp! macro is a shorthand for the two helpers above:

// Serialise value as JSON and wrap with a status code
unwarp!(Status::Created, json => &item)

// Wrap any warp::Reply (html, plain text, etc.) with a status code
unwarp!(Status::Ok, warp::reply::html("pong"))

// Shorthand for Unwarp::json(&value)
unwarp!(my_struct)

Why json =>? Both unwarp!(status, json_val) and unwarp!(status, string_val) look identical to the macro engine — it matches on token structure, not types. The json => keyword discriminator makes the two arms syntactically distinct so the right one is always chosen.


Status enum

A typed representation of common HTTP status codes, convertible to warp::http::StatusCode via Into.

Variant Code
Status::Ok 200
Status::Created 201
Status::Accepted 202
Status::NoContent 204
Status::MovedPermanently 301
Status::Found 302
Status::NotModified 304
Status::TemporaryRedirect 307
Status::PermanentRedirect 308
Status::BadRequest 400
Status::Unauthorized 401
Status::Forbidden 403
Status::NotFound 404
Status::MethodNotAllowed 405
Status::Conflict 409
Status::Gone 410
Status::UnprocessableEntity 422
Status::TooManyRequests 429
Status::InternalServerError 500
Status::NotImplemented 501
Status::BadGateway 502
Status::ServiceUnavailable 503
Status::GatewayTimeout 504

Full example

use serde::{Deserialize, Serialize};
use unwarp::prelude::*;  // brings Unwarp, RouteBuilder, Status, unwarp!, and warp into scope

#[derive(Deserialize, Serialize)]
struct Message { text: String }

#[derive(Deserialize)]
struct Filter { prefix: Option<String> }

#[tokio::main]
async fn main() {
    let mut server = Unwarp::new();

    // GET /ping
    server.route(
        RouteBuilder::get("ping")
            .handle(|| async {
                Unwarp::with_status(Status::Ok, warp::reply::html("pong"))
            })
    );

    // POST /echo  — JSON body, returns 201 Created
    server.route(
        RouteBuilder::post("echo")
            .json::<Message>()
            .handle(|msg: Message| async move {
                Unwarp::json_with_status(Status::Created, &msg)
            })
    );

    // GET /search?prefix=foo  — query params
    server.route(
        RouteBuilder::get("search")
            .query::<Filter>()
            .handle(|f: Filter| async move {
                let result = f.prefix.unwrap_or_default();
                Unwarp::json(&Message { text: result })
            })
    );

    server.serve(([0, 0, 0, 0], 3030)).await;
}

Prelude

Import everything you need in one line:

use unwarp::prelude::*;
// re-exports: Unwarp, unwarp!, RouteBuilder, Status
// also re-exports: warp  (so warp::reply::*, warp::Rejection, etc. work without a warp dep)

License

MIT — see LICENSE.