ohkami - [狼] means wolf in Japanese - is simple and macro free web framework for Rust.
Features
- simple: Less things to learn / Less code to write / Less time to hesitate.
- macro free: No need for using macros.
- async handlers
- easy error handling
0.6 → 0.7
Add JSON attribute that means "I can be handled as JSON". This uses serde's derive macros internally, so serde = { version = "1.0", features = ["derive"] } is neede in your Cargo.toml for this.
Only structs that has this attribute can be passed to handlers as reuqest body.
use ohkami::prelude::*;
#[JSON]
struct User {
id: u64,
name: String,
}
async fn handler(c: Context, payload: User) -> Result<Response> {
}
Quick start
- Add dependencies:
[dependencies]
ohkami = "0.7.0"
- Write your first code with ohkami:
use ohkami::prelude::*;
fn main() -> Result<()> {
Ohkami::default()
.GET("/", || async {
Response::OK("Hello, world!")
})
.howl(":3000")
}
- If you're interested in ohkami, learn more by examples and documentation !
Snippets
handle query params
let name: &str = c.req.query("name")?;
let count: usize = c.req.query("count")?;
handle request body
fn main() -> Result<()> {
Ohkami::default()
.GET("/api/users", reflect)
.GET("/api/users/name", reflect_name)
.howl(":3000")
}
#[JSON]
struct User {
id: i64,
name: String,
}
async fn reflect(user: User) -> Result<Response> {
Response::OK(user)
}
async fn reflect_name(user: User) -> Result<Response> {
let name = user.name;
Response::OK(name)
}
handle path params
fn main() -> Result<()> {
Ohkami::default()
.GET("/sleepy/:time/:name", sleepy_hello)
.howl("localhost:8080")
}
async fn sleepy_hello(time: u64, name: String) -> Result<Response> {
(time < 30)
._else(|| Response::BadRequest("sleeping time (sec) must be less than 30."))?;
std::thread::sleep(
std::time::Duration::from_secs(time)
);
Response::OK(format!("Hello {name}, I'm extremely sleepy..."))
}
grouping handlers on the same path (like axum)
use serde::{Serialize, Deserialize};
use ohkami::{
prelude::*,
group::{GET, POST} };
#[JSON]
struct User {
id: usize,
name: String,
}
fn main() -> Result<()> {
Ohkami::default()
.GET("/", || async {
Response::OK("Hello!")
})
.route("/api",
GET(hello_api).POST(reflect)
)
.howl(":3000")
}
async fn hello_api() -> Result<Response> {
Response::OK("Hello, api!")
}
async fn reflect(payload: User) -> Result<Response> {
Response::OK(payload)
}
parse request headers
let host = c.req.header(Header::Host)?;
async fn reflect_header_custom(c: Context) -> Result<Response> {
let custom_header_value = c.req.header("X-Custom")?;
c.OK(format!("`X-Custom`'s value is {custom_header_value}"))
}
add response headers
c.add_header(Header::AccessControlAllowOrigin, "mydomain:8000");
c.add_header("Access-Control-Allow-Origin", "mydomain:8000");
use ohkami::prelude::*;
use Header::{AccessControlAllowOrigin};
async fn cors(c: Context) -> Context {
c.add_header(AccessControlAllowOrigin, "mydomain:8000");
c
}
fn main() -> Result<()> {
let middleware = Middleware::new()
.ANY("/api/*", cors);
OK response with text/plain
Response::OK("Hello, world!")
c.OK("Hello, world!")
OK response with application/json
Response::OK(json!{"ok": true})
c.OK(json!{"ok": true})
#[JSON]
struct User {
id: u64,
name: String,
}
let user = User { id: 1, name: String::from("John") };
Response::OK(user)
c.OK(user)
handle errors
make_ohkami_result()?;
make_ohkami_result()
._else(|e| e.error_context("failed to get user data"))?;
make_ohkami_result()
._else(|_| Response::InternalServerError("can't get user"))?;
._else(|_| Response::InternalServerError(None))?;
make_some_result()
._else(|e| Response::InternalServerError(e.to_string()))?;
make_some_result()
._else(|_| Response::InternalServerError(None))?;
handle Option values
let handler = self.handler.as_ref()
._else(|| Response::NotFound("handler not found"))?;
._else(|| Response::NotFound(None))?;
assert boolean conditions
(count < 10)
._else(|| Response::BadRequest("`count` must be less than 10"))?;
._else(|| Response::BadRequest(None))?;
log config
add tracing and tracing_subscriber to your Cargo.toml.
fn main() -> Result<()> {
let config = Config {
log_subscribe: Some(
tracing_subscriber::fmt()
.with_max_level(tracing::Level::TRACE)
),
..Default::default()
};
Ohkami::with(config)
.GET("/", || async {Response::OK("Hello!")})
}
DB config
eneble one of following pairs of features:
sqlx and postgres
sqlx and mysql
let config = Config {
db_profile: DBprofile {
options: PgPoolOptions::new().max_connections(20),
url: DB_URL.as_str(),
},
..Default::default()
};
use sqlx
eneble one of following pairs of features:
sqlx and postgres
sqlx and mysql
let user = sqlx::query_as::<_, User>(
"SELECT id, name FROM users WHERE id = $1"
).bind(1)
.fetch_one(c.pool())
.await?;
use middlewares
fn main() -> Result<()> {
let middleware = Middleware::new()
.ANY("*", |c| async {
tracing::info!("Hello, middleware!");
c
});
Ohkami::with(middleware)
.GET("/", || async {
Response::OK("Hello!")
})
.howl("localhost:3000")
}
fn main() -> Result<()> {
let config = Config {
log_subscribe: Some(
tracing_subscriber::fmt()
.with_max_level(tracing::Level::TRACE)
),
..Default::default()
};
let middleware = Middleware::new()
.ANY("*", |c| async {
tracing::info!("Hello, middleware!");
c
});
let thirdparty_middleware = some_external_crate::x;
Ohkami::with(config.and(middleware).and(x))
.GET("/", || async {
Response::OK("Hello!")
})
.howl("localhost:3000")
}
test
- split setup process from
main function:
fn server() -> Ohkami {
Ohkami::default()
.GET("/", || async {Response::OK("Hello!")})
}
fn main() -> Result<()> {
server().howl(":3000")
}
- import
testing::Test and other utils
#[cfg(test)]
mod test {
use ohkami::{Ohkami, response::Response, testing::{Test, Request, Method}};
use once_cell::sync::Lazy;
static SERVER: Lazy<Ohkami> = Lazy::new(|| super::server());
#[test]
fn test_hello() {
let req = Request::new(Method::GET, "/");
SERVER.assert_to_res(&req, Response::OK("Hello!"));
}
}
Development
ohkami is not for producntion use.
Please give me your feedback ! → GetHub issue
License
This project is licensed under MIT LICENSE (LICENSE-MIT or https://opensource.org/licenses/MIT).