{"id":13611240,"url":"https://github.com/ohkami-rs/ohkami","last_synced_at":"2025-04-08T09:04:33.276Z","repository":{"id":64781381,"uuid":"570783575","full_name":"ohkami-rs/ohkami","owner":"ohkami-rs","description":"Ohkami - intuitive and declarative web framework for Rust","archived":false,"fork":false,"pushed_at":"2025-03-30T07:51:08.000Z","size":2671,"stargazers_count":184,"open_issues_count":20,"forks_count":9,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-04-01T08:29:16.023Z","etag":null,"topics":["async","async-std","aws-lambda","cloudflare","cloudflare-workers","glommio","graceful-shutdown","http","lambda","nio","ohkami","rust","server","server-sent-events","smol","tokio","web","web-development","web-framework","websocket"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ohkami-rs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-11-26T05:29:35.000Z","updated_at":"2025-03-23T08:04:49.000Z","dependencies_parsed_at":"2024-01-19T17:38:33.708Z","dependency_job_id":"7d5932b4-976b-4bd8-ae25-ea2d940d3f41","html_url":"https://github.com/ohkami-rs/ohkami","commit_stats":{"total_commits":1377,"total_committers":3,"mean_commits":459.0,"dds":0.002904865649963728,"last_synced_commit":"532223fba07a05e378735d01c115bc242aee272f"},"previous_names":["ohkami-rs/ohkami","kana-rus/ohkami"],"tags_count":17,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ohkami-rs%2Fohkami","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ohkami-rs%2Fohkami/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ohkami-rs%2Fohkami/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ohkami-rs%2Fohkami/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ohkami-rs","download_url":"https://codeload.github.com/ohkami-rs/ohkami/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247809964,"owners_count":20999816,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","async-std","aws-lambda","cloudflare","cloudflare-workers","glommio","graceful-shutdown","http","lambda","nio","ohkami","rust","server","server-sent-events","smol","tokio","web","web-development","web-framework","websocket"],"created_at":"2024-08-01T19:01:53.247Z","updated_at":"2025-04-08T09:04:33.225Z","avatar_url":"https://github.com/ohkami-rs.png","language":"Rust","funding_links":[],"categories":["Rust"],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n    \u003ch1\u003eOhkami\u003c/h1\u003e\n    Ohkami \u003cem\u003e- [狼] wolf in Japanese -\u003c/em\u003e is intuitive and declarative web framework.\n\u003c/div\u003e\n\n\u003cbr\u003e\n\n- *macro-less and type-safe* APIs for intuitive and declarative code\n- *various runtimes* are supported：`tokio`, `async-std`, `smol`, `nio`, `glommio` and `worker` (Cloudflare Workers), `lambda` (AWS Lambda)\n- extremely fast, no-network testing, well-structured middlewares, Server-Sent Events, WebSocket, highly integrated OpenAPI document generation, ...\n\n\u003cdiv align=\"right\"\u003e\n    \u003ca href=\"https://github.com/ohkami-rs/ohkami/blob/main/LICENSE\"\u003e\u003cimg alt=\"License\" src=\"https://img.shields.io/crates/l/ohkami.svg\" /\u003e\u003c/a\u003e\n    \u003ca href=\"https://github.com/ohkami-rs/ohkami/actions\"\u003e\u003cimg alt=\"build check status of ohkami\" src=\"https://github.com/ohkami-rs/ohkami/actions/workflows/CI.yml/badge.svg\"/\u003e\u003c/a\u003e\n    \u003ca href=\"https://crates.io/crates/ohkami\"\u003e\u003cimg alt=\"crates.io\" src=\"https://img.shields.io/crates/v/ohkami\" /\u003e\u003c/a\u003e\n\u003c/div\u003e\n\n\u003cbr\u003e\n\n## Quick Start\n\n1. Add to `dependencies` :\n\n```toml\n[dependencies]\nohkami = { version = \"0.23\", features = [\"rt_tokio\"] }\ntokio  = { version = \"1\",    features = [\"full\"] }\n```\n\n2. Write your first code with Ohkami : [examples/quick_start](https://github.com/ohkami-rs/ohkami/blob/main/examples/quick_start/src/main.rs)\n\n```rust,no_run\nuse ohkami::prelude::*;\nuse ohkami::typed::status;\n\nasync fn health_check() -\u003e status::NoContent {\n    status::NoContent\n}\n\nasync fn hello(name: \u0026str) -\u003e String {\n    format!(\"Hello, {name}!\")\n}\n\n#[tokio::main]\nasync fn main() {\n    Ohkami::new((\n        \"/healthz\"\n            .GET(health_check),\n        \"/hello/:name\"\n            .GET(hello),\n    )).howl(\"localhost:3000\").await\n}\n```\n\n3. Run and check the behavior :\n\n```sh\n$ cargo run\n```\n```sh\n$ curl http://localhost:3000/healthz\n$ curl http://localhost:3000/hello/your_name\nHello, your_name!\n```\n\n\u003cbr\u003e\n\n## Feature flags\n\n### `\"rt_tokio\"`, `\"rt_async-std\"`, `\"rt_smol\"`, `\"rt_nio\"`, `\"rt_glommio\"` : native async runtime\n\n- [tokio](https://github.com/tokio-rs/tokio)\n- [async-std](https://github.com/async-rs/async-std)\n- [smol](https://github.com/smol-rs/smol)\n- [nio](https://github.com/nurmohammed840/nio)\n- [glommio](https://github.com/DataDog/glommio)\n\n### `\"rt_worker\"` : Cloudflare Workers\n\nWorks with [worker](https://crates.io/crates/worker) crate.\n\n```sh\nnpm create cloudflare ＜project dir＞ -- --template https://github.com/ohkami-rs/ohkami-templates/worker\n```\n\nthen `＜project dir＞` will have `wrangler.toml`, `package.json` and a Rust library crate.\n\nA `#[ohkami::worker]` (async/sync) fn returning `Ohkami` is the Worker definition.\n\nLocal dev by `npm run dev` and deploy by `npm run deploy` !\n\nSee README of [template](https://github.com/ohkami-rs/ohkami-templates/tree/main/worker) for details.\n\nOr, here are [Workers + OpenAPI template](https://github.com/ohkami-rs/ohkami-templates/tree/main/worker-openapi) and [Workers + SPA with Yew template](https://github.com/ohkami-rs/ohkami-templates/tree/main/worker_yew_spa).\n\n### `\"rt_lambda\"` : AWS Lambda\n\n**experimental**\n\n* Both `Function URLs` and `API Gateway` are supported\n* WebSocket is not supported now\n* Please let us know any bugs or unexpected behavior on [PR](https://github.com/ohkami-rs/ohkami/pulls)!\n\nWorks with [lambda_runtime](https://crates.io/crates/lambda_runtime) crate ( and tokio ).\n\n[cargo lambda](https://crates.io/crates/cargo-lambda) will be good partner.\n\nLet's :\n\n```sh\ncargo lambda new ＜project dir＞ --template https://github.com/ohkami-rs/ohkami-templates\n```\n\n`lambda_runtime::run(your_ohkami)` make `you_ohkami` run on Lambda Function.\n\nLocal dev by\n\n```sh\ncargo lambda watch\n```\n\nand deploy by\n\n```sh\ncargo lambda build --release [--compiler cargo] [and more]\ncargo lambda deploy [--role ＜arn-of-a-iam-role＞] [and more]\n```\n\nSee\n\n* README of [template](https://github.com/ohkami-rs/ohkami-templates/tree/main/template)\n* [Cargo Lambda document](https://www.cargo-lambda.info)\n\nfor details.\n\n### `\"sse\"` : Server-Sent Events\n\nOhkami responds with HTTP/1.1 `Transfer-Encoding: chunked`.\\\nUse some reverse proxy to do with HTTP/2,3.\n\n```rust,no_run\nuse ohkami::prelude::*;\nuse ohkami::sse::DataStream;\nuse tokio::time::{sleep, Duration};\n\nasync fn handler() -\u003e DataStream {\n    DataStream::new(|mut s| async move {\n        s.send(\"starting streaming...\");\n        for i in 1..=5 {\n            sleep(Duration::from_secs(1)).await;\n            s.send(format!(\"MESSAGE #{i}\"));\n        }\n        s.send(\"streaming finished!\");\n    })\n}\n\n#[tokio::main]\nasync fn main() {\n    Ohkami::new((\n        \"/sse\".GET(handler),\n    )).howl(\"localhost:3020\").await\n}\n```\n\n### `\"ws\"` : WebSocket\n\nOhkami only handles `ws://`.\\\nUse some reverse proxy to do with `wss://`.\n\n```rust,no_run\nuse ohkami::prelude::*;\nuse ohkami::ws::{WebSocketContext, WebSocket, Message};\n\nasync fn echo_text(ctx: WebSocketContext\u003c'_\u003e) -\u003e WebSocket {\n    ctx.upgrade(|mut conn| async move {\n        while let Ok(Some(Message::Text(text))) = conn.recv().await {\n            conn.send(text).await.expect(\"failed to send text\");\n        }\n    })\n}\n\n#[tokio::main]\nasync fn main() {\n    Ohkami::new((\n        \"/ws\".GET(echo_text),\n    )).howl(\"localhost:3030\").await\n}\n```\n\n* On `\"rt_worker\"`, both normal ( stateless ) WebSocket and WebSocket on Durable Object are available!\n* On `\"rt_lambda\"`, WebSocket is currently not supported.\n\n### `\"openapi\"` : OpenAPI document generation\n\n`\"openapi\"` provides highly integrated OpenAPI support.\n\nThis enables **macro-less**, *as consistent as possible* OpenAPI document generation, where most of the consistency between document and behavior is automatically assured by Ohkami's internal work.\n\nOnly you have to\n\n- Derive `openapi::Schema` for all your schema structs\n- Make your `Ohkami` call `.generate(openapi::OpenAPI { ... })`\n\nto generate consistent OpenAPI document.\n\nYou don't need to take care of writing accurate methods, paths, parameters, contents, ... for this OpenAPI feature; All they are done by Ohkami.\n\nOf course, you can flexibly customize schemas ( by hand-implemetation of `Schema` ), descriptions or other parts ( by `#[operation]` attribute and `openapi_*` hooks ).\n\n```rust,ignore\nuse ohkami::prelude::*;\nuse ohkami::typed::status;\nuse ohkami::openapi;\n\n// Derive `Schema` trait to generate\n// the schema of this struct in OpenAPI document.\n#[derive(Deserialize, openapi::Schema)]\nstruct CreateUser\u003c'req\u003e {\n    name: \u0026'req str,\n}\n\n#[derive(Serialize, openapi::Schema)]\n// `#[openapi(component)]` to define it as component\n// in OpenAPI document.\n#[openapi(component)]\nstruct User {\n    id: usize,\n    name: String,\n}\n\nasync fn create_user(\n    JSON(CreateUser { name }): JSON\u003cCreateUser\u003c'_\u003e\u003e\n) -\u003e status::Created\u003cJSON\u003cUser\u003e\u003e {\n    status::Created(JSON(User {\n        id: 42,\n        name: name.to_string()\n    }))\n}\n\n// (optionally) Set operationId, summary,\n// or override descriptions by `operation` attribute.\n#[openapi::operation({\n    summary: \"...\",\n    200: \"List of all users\",\n})]\n/// This doc comment is used for the\n/// `description` field of OpenAPI document\nasync fn list_users() -\u003e JSON\u003cVec\u003cUser\u003e\u003e {\n    JSON(vec![])\n}\n\n#[tokio::main]\nasync fn main() {\n    let o = Ohkami::new((\n        \"/users\"\n            .GET(list_users)\n            .POST(create_user),\n    ));\n\n    // This make your Ohkami spit out `openapi.json`\n    // ( the file name is configurable by `.generate_to` ).\n    o.generate(openapi::OpenAPI {\n        title: \"Users Server\",\n        version: \"0.1.0\",\n        servers: \u0026[\n            openapi::Server::at(\"localhost:5000\"),\n        ]\n    });\n\n    o.howl(\"localhost:5000\").await;\n}\n```\n\n- Currently, only JSON is supported as the document format.\n- When the binary size matters, you should prepare a feature flag activating `ohkami/openapi` in your package, and put all your codes around `openapi` behind that feature via `#[cfg(feature = ...)]` or `#[cfg_attr(feature = ...)]`.\n- In `rt_worker`, `.generate` is not available because `Ohkami` can't have access to your local filesystem by `wasm32` binary on Minifalre. So ohkami provides [a CLI tool](./scripts/workers_openapi.js) to generate document from `#[ohkami::worker] Ohkami` with `openapi` feature.\n\n### `\"nightly\"` : nightly-only functionalities\n\n- try response\n\n\u003cbr\u003e\n\n## Snippets\n\n### Typed payload\n\n*builtin payload* : `JSON`, `Text`, `HTML`, `URLEncoded`, `Multipart`\n\n```rust\nuse ohkami::prelude::*;\nuse ohkami::typed::status;\n\n/* Deserialize for request */\n#[derive(Deserialize)]\nstruct CreateUserRequest\u003c'req\u003e {\n    name:     \u0026'req str,\n    password: \u0026'req str,\n}\n\n/* Serialize for response */\n#[derive(Serialize)]\nstruct User {\n    name: String,\n}\n\nasync fn create_user(\n    JSON(req): JSON\u003cCreateUserRequest\u003c'_\u003e\u003e\n) -\u003e status::Created\u003cJSON\u003cUser\u003e\u003e {\n    status::Created(JSON(\n        User {\n            name: String::from(req.name)\n        }\n    ))\n}\n```\n\n### Typed params\n\n```rust,no_run\nuse ohkami::prelude::*;\n\n#[tokio::main]\nasync fn main() {\n    Ohkami::new((\n        \"/hello/:name/:n\"\n            .GET(hello_n),\n        \"/hello/:name\"\n            .GET(hello),\n        \"/search\"\n            .GET(search),\n    )).howl(\"localhost:5000\").await\n}\n\nasync fn hello(name: \u0026str) -\u003e String {\n    format!(\"Hello, {name}!\")\n}\n\nasync fn hello_n((name, n): (\u0026str, usize)) -\u003e String {\n    vec![format!(\"Hello, {name}!\"); n].join(\" \")\n}\n\n#[derive(Deserialize)]\nstruct SearchQuery\u003c'q\u003e {\n    #[serde(rename = \"q\")]\n    keyword: \u0026'q str,\n    lang:    \u0026'q str,\n}\n\n#[derive(Serialize)]\nstruct SearchResult {\n    title: String,\n}\n\nasync fn search(\n    Query(query): Query\u003cSearchQuery\u003c'_\u003e\u003e\n) -\u003e JSON\u003cVec\u003cSearchResult\u003e\u003e {\n    JSON(vec![\n        SearchResult { title: String::from(\"ohkami\") },\n    ])\n}\n```\n\n### Middlewares\n\nOhkami's request handling system is called \"**fang**s\", and middlewares are implemented on this.\n\n*builtin fang* :\n\n- `Context` *( typed interaction with reuqest context )*\n- `CORS`, `JWT`, `BasicAuth`\n- `Timeout` *( native runtime )*\n- `Enamel` *( experimantal; security headers )*\n\n```rust,no_run\nuse ohkami::prelude::*;\n\n#[derive(Clone)]\nstruct GreetingFang(usize);\n\n/* utility trait; automatically impl `Fang` trait */\nimpl FangAction for GreetingFang {\n    async fn fore\u003c'a\u003e(\u0026'a self, req: \u0026'a mut Request) -\u003e Result\u003c(), Response\u003e {\n        let Self(id) = self;\n        println!(\"[{id}] Welcome request!: {req:?}\");\n        Ok(())\n    }\n    async fn back\u003c'a\u003e(\u0026'a self, res: \u0026'a mut Response) {\n        let Self(id) = self;\n        println!(\"[{id}] Go, response!: {res:?}\");\n    }\n}\n\n#[tokio::main]\nasync fn main() {\n    Ohkami::new((\n        // register fangs to a Ohkami\n        GreetingFang(1),\n        \n        \"/hello\"\n            .GET(|| async {\"Hello, fangs!\"})\n            .POST((\n                // register *local fangs* to a handler\n                GreetingFang(2),\n                || async {\"I'm `POST /hello`!\"}\n            ))\n    )).howl(\"localhost:3000\").await\n}\n```\n\n### Database connection management with `Context`\n\n```rust,no_run\nuse ohkami::prelude::*;\nuse ohkami::typed::status;\nuse sqlx::postgres::{PgPoolOptions, PgPool};\n\n#[tokio::main]\nasync fn main() {\n    let pool = PgPoolOptions::new()\n        .connect(\"postgres://ohkami:password@localhost:5432/db\").await\n        .expect(\"failed to connect\");\n\n    Ohkami::new((\n        Context::new(pool),\n        \"/users\".POST(create_user),\n    )).howl(\"localhost:5050\").await\n}\n\nasync fn create_user(\n    Context(pool): Context\u003c'_, PgPool\u003e,\n) -\u003e status::Created {\n    //...\n\n    status::Created(())\n}\n```\n\n### Static directory serving\n\n```rust,no_run\nuse ohkami::prelude::*;\n\n#[tokio::main]\nasync fn main() {\n    Ohkami::new((\n        \"/\".Dir(\"./dist\"),\n    )).howl(\"0.0.0.0:3030\").await\n}\n```\n\n### File upload\n\n```rust,no_run\nuse ohkami::prelude::*;\nuse ohkami::typed::status;\nuse ohkami::format::{Multipart, File};\n\n#[derive(Deserialize)]\nstruct FormData\u003c'req\u003e {\n    #[serde(rename = \"account-name\")]\n    account_name: Option\u003c\u0026'req str\u003e,\n    pics: Vec\u003cFile\u003c'req\u003e\u003e,\n}\n\nasync fn post_submit(\n    Multipart(data): Multipart\u003cFormData\u003c'_\u003e\u003e\n) -\u003e status::NoContent {\n    println!(\"\\n\\\n        ===== submit =====\\n\\\n        [account name] {:?}\\n\\\n        [  pictures  ] {} files (mime: [{}])\\n\\\n        ==================\",\n        data.account_name,\n        data.pics.len(),\n        data.pics.iter().map(|f| f.mimetype).collect::\u003cVec\u003c_\u003e\u003e().join(\", \"),\n    );\n\n    status::NoContent\n}\n```\n\n### Pack of Ohkamis\n\n```rust,no_run\nuse ohkami::prelude::*;\nuse ohkami::typed::status;\n\n#[derive(Serialize)]\nstruct User {\n    name: String\n}\n\nasync fn list_users() -\u003e JSON\u003cVec\u003cUser\u003e\u003e {\n    JSON(vec![\n        User { name: String::from(\"actix\") },\n        User { name: String::from(\"axum\") },\n        User { name: String::from(\"ohkami\") },\n    ])\n}\n\nasync fn create_user() -\u003e status::Created\u003cJSON\u003cUser\u003e\u003e {\n    status::Created(JSON(User {\n        name: String::from(\"ohkami web framework\")\n    }))\n}\n\nasync fn health_check() -\u003e status::NoContent {\n    status::NoContent\n}\n\n#[tokio::main]\nasync fn main() {\n    // ...\n\n    let users_ohkami = Ohkami::new((\n        \"/\"\n            .GET(list_users)\n            .POST(create_user),\n    ));\n\n    Ohkami::new((\n        \"/healthz\"\n            .GET(health_check),\n        \"/api/users\"\n            .By(users_ohkami), // nest by `By`\n    )).howl(\"localhost:5000\").await\n}\n```\n\n### Testing\n\n```rust\nuse ohkami::prelude::*;\nuse ohkami::testing::*; // \u003c--\n\nfn hello_ohkami() -\u003e Ohkami {\n    Ohkami::new((\n        \"/hello\".GET(|| async {\"Hello, world!\"}),\n    ))\n}\n\n#[cfg(test)]\n#[tokio::test]\nasync fn test_my_ohkami() {\n    let t = hello_ohkami().test();\n\n    let req = TestRequest::GET(\"/\");\n    let res = t.oneshot(req).await;\n    assert_eq!(res.status(), Status::NotFound);\n\n    let req = TestRequest::GET(\"/hello\");\n    let res = t.oneshot(req).await;\n    assert_eq!(res.status(), Status::OK);\n    assert_eq!(res.text(), Some(\"Hello, world!\"));\n}\n```\n\n\u003cbr\u003e\n\n## Supported protocols\n\n- [x] HTTP/1.1\n- [ ] HTTP/2\n- [ ] HTTP/3\n- [ ] HTTPS\n- [x] Server-Sent Events\n- [x] WebSocket\n\n## MSRV ( Minimum Supported Rust Version )\n\nLatest stable\n\n## License\n\nohkami is licensed under MIT LICENSE ( [LICENSE](https://github.com/ohkami-rs/ohkami/blob/main/LICENSE) or [https://opensource.org/licenses/MIT](https://opensource.org/licenses/MIT) ).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fohkami-rs%2Fohkami","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fohkami-rs%2Fohkami","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fohkami-rs%2Fohkami/lists"}