11 stable releases

Uses new Rust 2024

new 2.5.2 Feb 20, 2026
2.4.0 Feb 20, 2026
1.1.0 Feb 18, 2026

#1759 in Network programming

MIT/Apache

29KB
608 lines

Nimble

A simple and elegant Rust web framework inspired by Express, built on Hyper.

Features

  • Simple & Intuitive - Express-like route definition style
  • Hyper-Powered - Built on a reliable HTTP library
  • Zero-Cost Abstractions - Leverages Rust's powerful type system
  • Type Safe - Compile-time guarantee of correct types for routes and handlers
  • Practical Utilities - Built-in response types for JSON, HTML, file serving, redirects, etc.
  • Automatic Static File Serving - Automatically mounts files from the ./static directory
  • CORS Support - Flexible CORS configuration with simple .cors() for development and detailed control for production

Quick Start

Add the dependency to your Cargo.toml:

[dependencies]
nimble-http = "2"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }  # If handling JSON

Create a simple web application:

use nimble_http::{Router, get, post, post_json, Html, Json, Redirect, Text};
use serde::Deserialize;

#[derive(Deserialize)]
struct User {
	name: String,
	age: u8,
}

#[tokio::main]
async fn main() {
	let app = Router::new()
		// GET root path, returns HTML
		.route("/", get(|_| async {
			Html("<h1>Hello World</h1>".to_string())
		}))
		// GET returns JSON
		.route("/json", get(|_| async {
			Json(vec!["apple", "banana", "orange"])
		}))
		// POST handles form (application/x-www-form-urlencoded)
		.route("/user", post(|params| async move {
			let name = params.get("name").unwrap_or(&"Anonymous".to_string()).clone();
			Text(format!("Hello, {}!", name))
		}))
		// POST handles JSON
		.route("/api/user", post_json(|user: User| async move {
			Json(format!("Created user: {}, age: {}", user.name, user.age))
		}))
		// Redirect to Baidu
		.route("/baidu", get(|_| async {
			Redirect("https://www.baidu.com".to_string())
		}));

	// Start the server
	app.run("127.0.0.1", 3000).await;
}

Routes

Nimble currently supports GET and POST methods, with POST further divided into regular form and JSON types.

use nimble_http::{get, post, post_json};

Router::new()
	.route("/", get(handler_get))
	.route("/submit", post(handler_post))
	.route("/api/data", post_json(handler_post_json));

Note: The current version does not support path parameters (e.g., /users/:id) or methods like PUT and DELETE.

Response Types

Nimble provides various built-in response types, all implementing the IntoResponse trait:

use nimble_http::{Html, Json, Text, Redirect, File, StatusCode};

// HTML response
Html("<h1>Title</h1>".to_string())

// JSON response (requires the type to implement Serialize)
Json(vec!["apple", "banana", "orange"])

// Plain text response
Text("Hello".to_string())

// Temporary redirect (302)
Redirect("https://example.com".to_string())

// Permanent redirect (301)
Redirect::perm("https://example.com".to_string())

// File response (first parameter: file path, second: force download)
File("static/image.jpg".to_string(), false)   // Display directly
File("file.zip".to_string(), true)            // Download as attachment

// Status code only (empty response)
StatusCode::NOT_FOUND

// Content with request headers
(
	[
		(header::CONTENT_TYPE, "text/html; charset=utf-8"),
		(header::CACHE_CONTROL, "no-cache"),
		(header::HeaderName::from_static("x-powered-by"), "Nimble"),
	],
	"<h1>Hello</h1>".to_string()
)

Additionally, the following types automatically implement IntoResponse:

  • &'static str
  • String
  • Vec<u8>
  • ()
  • Result<T, E> where both T and E implement IntoResponse

Static File Serving

Router::new() automatically scans the ./static folder in your project root and maps all files to routes.

For example, with the following directory structure:

├── static/
│   ├── css/
│   │   └── style.css
│   ├── js/
│   │   └── app.js
│   └── images/
|       └── logo.png
└── src/
    └── main.rs

After starting the application, you can access files via:

  • http://localhost:3000/css/style.css
  • http://localhost:3000/js/app.js
  • http://localhost:3000/images/logo.png

File Download

Static file routes support force download via the query parameter ?download=true:

http://localhost:3000/images/logo.png?download=true

CORS Configuration

Nimble provides flexible CORS (Cross-Origin Resource Sharing) support:

Development Mode - One Line Setup

use nimble_http::{Router, get};

#[tokio::main]
async fn main() {
    Router::new()
        .route("/api/hello", get(|_| async { "Hello" }))
        .cors()  // Allows all origins, perfect for development
        .run("127.0.0.1", 3000)
        .await;
}

Production Mode - Fine-grained Control

use nimble_http::{Router, get, Cors};
use std::time::Duration;

#[tokio::main]
async fn main() {
    let cors = Cors::new()
        .allow_origins(["https://myapp.com", "https://admin.myapp.com"])
        .allow_methods(["GET", "POST", "PUT", "DELETE"])
        .allow_headers(["Content-Type", "Authorization"])
        .allow_credentials(true)
        .max_age(Duration::from_secs(3600));  // Cache preflight for 1 hour

    Router::new()
        .route("/api/user", get(get_user))
        .with_cors(cors)
        .run("127.0.0.1", 3000)
        .await;
}

How CORS Works

When a browser makes a cross-origin request, it first sends an OPTIONS preflight request. Nimble automatically handles these and returns the appropriate CORS headers based on your configuration.

// The CORS headers returned for a successful preflight request:
// Access-Control-Allow-Origin: https://myapp.com
// Access-Control-Allow-Methods: GET, POST
// Access-Control-Allow-Headers: Content-Type, Authorization
// Access-Control-Max-Age: 3600
// Access-Control-Allow-Credentials: true

Testing CORS with curl

# Test preflight request
curl -X OPTIONS http://localhost:3000/api/hello \
  -H "Origin: https://myapp.com" \
  -H "Access-Control-Request-Method: GET" \
  -v

# Test actual request
curl http://localhost:3000/api/hello \
  -H "Origin: https://myapp.com" \
  -v

CORS Configuration Methods

Method Description Example
.cors() Quick setup for development, allows all origins .cors()
.with_cors(cors) Apply custom CORS configuration .with_cors(my_cors)

Cors Builder Methods

Method Description Default
.allow_origins(origins) Specify allowed origins None (allows all)
.allow_methods(methods) Specify allowed HTTP methods ["GET", "POST", "OPTIONS"]
.allow_headers(headers) Specify allowed request headers ["Content-Type", "Authorization"]
.allow_credentials(true) Allow cookies/auth headers false
.max_age(duration) Cache preflight response None

Debug Logging

Add .debug() to enable request logging:

Router::new()
    .route("/", get(|_| async { "Hello" }))
    .debug()  // Enable logging
    .run("127.0.0.1", 3000)
    .await;

Output:

Nimble server running on http://127.0.0.1:3000
192.168.1.100 GET / -> 200 (OK)
192.168.1.100 GET /api/users -> 200 (OK)
192.168.1.100 GET /nonexistent -> 404 (Not Found)

Request Parameters

GET Requests

GET handlers receive a HashMap<String, String> containing query string parameters from the URL.

use std::collections::HashMap;

async fn search(params: HashMap<String, String>) -> impl IntoResponse {
	let query = params.get("q").unwrap_or(&"".to_string());
	let page = params.get("page").and_then(|p| p.parse::<u32>().ok()).unwrap_or(1);
	Text(format!("Search: {}, Page: {}", query, page))
}

Router::new().route("/search", get(search));

POST Forms

Regular POST handlers also receive a HashMap<String, String>, with data from the application/x-www-form-urlencoded request body.

async fn login(params: HashMap<String, String>) -> impl IntoResponse {
	let username = params.get("username").cloned().unwrap_or_default();
	let password = params.get("password").cloned().unwrap_or_default();
	// Process login...
	Text("Login successful".to_string())
}

POST JSON

Use post_json to automatically deserialize JSON request bodies into the specified type (must implement Deserialize).

use serde::Deserialize;

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

async fn create_user(data: CreateUser) -> impl IntoResponse {
	// Use data.name and data.email
	Json(format!("Created user: {}", data.name))
}

Router::new().route("/users", post_json(create_user));

Error Handling

By returning Result<T, E>, you can easily handle errors, where both T and E must implement IntoResponse.

use nimble_http::{Text, StatusCode};

async fn get_user() -> Result<Text, StatusCode> {
	// Simulate user lookup
	let user = find_user().await.ok_or(StatusCode::NOT_FOUND)?;
	Ok(Text(format!("Username: {}", user)))
}

Router::new().route("/profile", get(|_| get_user()));

License

This project is licensed under:

  • MIT
  • Apache-2.0

Author: Wang Xiaoyu Email: wxy6987@outlook.com

You are free to choose either license.

Dependencies

~9–13MB
~162K SLoC