{"id":13502911,"url":"https://github.com/salvo-rs/salvo","last_synced_at":"2025-05-13T17:03:45.823Z","repository":{"id":37105416,"uuid":"220185454","full_name":"salvo-rs/salvo","owner":"salvo-rs","description":"A powerful web framework built with a simplified design.","archived":false,"fork":false,"pushed_at":"2025-05-05T18:41:47.000Z","size":17147,"stargazers_count":3685,"open_issues_count":39,"forks_count":230,"subscribers_count":47,"default_branch":"main","last_synced_at":"2025-05-06T16:12:05.177Z","etag":null,"topics":["async","framework","http-server","rust","salvo","web"],"latest_commit_sha":null,"homepage":"https://salvo.rs","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/salvo-rs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":null,"patreon":null,"open_collective":"salvo","ko_fi":"chrislearn","tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"lfx_crowdfunding":null,"custom":["https://afdian.com/a/chrislearn"]}},"created_at":"2019-11-07T08:18:29.000Z","updated_at":"2025-05-06T12:04:05.000Z","dependencies_parsed_at":"2023-09-27T12:58:21.833Z","dependency_job_id":"f8181b3e-ee8e-4649-8bd2-9d1f437095e3","html_url":"https://github.com/salvo-rs/salvo","commit_stats":{"total_commits":2864,"total_committers":61,"mean_commits":"46.950819672131146","dds":"0.19378491620111726","last_synced_commit":"75f70aa263dff23a943a09445944ecfea52bfdee"},"previous_names":["kenorld/novel"],"tags_count":146,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/salvo-rs%2Fsalvo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/salvo-rs%2Fsalvo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/salvo-rs%2Fsalvo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/salvo-rs%2Fsalvo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/salvo-rs","download_url":"https://codeload.github.com/salvo-rs/salvo/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252811334,"owners_count":21807925,"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","framework","http-server","rust","salvo","web"],"created_at":"2024-07-31T22:02:29.875Z","updated_at":"2025-05-13T17:03:45.792Z","avatar_url":"https://github.com/salvo-rs.png","language":"Rust","funding_links":["https://opencollective.com/salvo","https://ko-fi.com/chrislearn","https://afdian.com/a/chrislearn"],"categories":["Libraries","Rust","库 Libraries","后端开发框架及项目","Web Framework","Programming","\u003ca name=\"Rust\"\u003e\u003c/a\u003eRust"],"sub_categories":["Web programming","网络编程 Web programming","管理面板","Rust 🦀"],"readme":"\u003cdiv align=\"center\"\u003e\r\n\u003cp\u003e\u003cimg alt=\"Salvo\" width=\"132\" style=\"max-width:40%;min-width:60px;\" src=\"https://salvo.rs/images/logo-text.svg\" /\u003e\u003c/p\u003e\r\n\u003cp\u003e\r\n    \u003ca href=\"https://github.com/salvo-rs/salvo/blob/main/README.md\"\u003eEnglish\u003c/a\u003e\u0026nbsp;\u0026nbsp;\r\n    \u003ca href=\"https://github.com/salvo-rs/salvo/blob/main/README.zh.md\"\u003e简体中文\u003c/a\u003e\u0026nbsp;\u0026nbsp;\r\n    \u003ca href=\"https://github.com/salvo-rs/salvo/blob/main/README.zh-hant.md\"\u003e繁體中文\u003c/a\u003e\r\n\u003c/p\u003e\r\n\u003cp\u003e\r\n\u003ca href=\"https://github.com/salvo-rs/salvo/actions\"\u003e\r\n    \u003cimg alt=\"build status\" src=\"https://github.com/salvo-rs/salvo/workflows/ci-linux/badge.svg\" /\u003e\r\n\u003c/a\u003e\r\n\u003ca href=\"https://github.com/salvo-rs/salvo/actions\"\u003e\r\n    \u003cimg alt=\"build status\" src=\"https://github.com/salvo-rs/salvo/workflows/ci-macos/badge.svg\" /\u003e\r\n\u003c/a\u003e\r\n\u003ca href=\"https://github.com/salvo-rs/salvo/actions\"\u003e\r\n    \u003cimg alt=\"build status\" src=\"https://github.com/salvo-rs/salvo/workflows/ci-windows/badge.svg\" /\u003e\r\n\u003c/a\u003e\r\n\u003ca href=\"https://codecov.io/gh/salvo-rs/salvo\"\u003e\u003cimg alt=\"codecov\" src=\"https://codecov.io/gh/salvo-rs/salvo/branch/main/graph/badge.svg\" /\u003e\u003c/a\u003e\r\n\u003cbr\u003e\r\n\u003ca href=\"https://crates.io/crates/salvo\"\u003e\u003cimg alt=\"crates.io\" src=\"https://img.shields.io/crates/v/salvo\" /\u003e\u003c/a\u003e\r\n\u003ca href=\"https://docs.rs/salvo\"\u003e\u003cimg alt=\"Documentation\" src=\"https://docs.rs/salvo/badge.svg\" /\u003e\u003c/a\u003e\r\n\u003ca href=\"https://crates.io/crates/salvo\"\u003e\u003cimg alt=\"Download\" src=\"https://img.shields.io/crates/d/salvo.svg\" /\u003e\u003c/a\u003e\r\n\u003ca href=\"https://github.com/rust-secure-code/safety-dance/\"\u003e\u003cimg alt=\"unsafe forbidden\" src=\"https://img.shields.io/badge/unsafe-forbidden-success.svg\" /\u003e\u003c/a\u003e\r\n\u003ca href=\"https://blog.rust-lang.org/2025/02/20/Rust-1.85.0.html\"\u003e\u003cimg alt=\"Rust Version\" src=\"https://img.shields.io/badge/rust-1.85%2B-blue\" /\u003e\u003c/a\u003e\r\n\u003cbr\u003e\r\n\u003ca href=\"https://salvo.rs\"\u003e\r\n    \u003cimg alt=\"Website\" src=\"https://img.shields.io/badge/https-salvo.rs-%23f00\" /\u003e\r\n\u003c/a\u003e\r\n\u003ca href=\"https://discord.gg/G8KfmS6ByH\"\u003e\r\n    \u003cimg src=\"https://img.shields.io/discord/1041442427006890014.svg?logo=discord\"\u003e\r\n\u003c/a\u003e\r\n\u003ca href=\"https://gitcode.com/salvo-rs/salvo\"\u003e\r\n    \u003cimg src=\"https://gitcode.com/salvo-rs/salvo/star/badge.svg\"\u003e\r\n\u003c/a\u003e\r\n\u003ca href=\"https://gurubase.io/g/salvo\"\u003e\u003cimg alt=\"Gurubase\" src=\"https://img.shields.io/badge/Gurubase-Ask%20Salvo%20Guru-006BFF\" /\u003e\u003c/a\u003e\r\n\u003c/p\u003e\r\n\u003c/div\u003e\r\n\r\nSalvo is an extremely simple and powerful Rust web backend framework. Only basic Rust knowledge is required to develop backend services.\r\n\r\n## 🎯 Features\r\n\r\n- Built with [Hyper 1](https://crates.io/crates/hyper) and [Tokio](https://crates.io/crates/tokio);\r\n- HTTP1, HTTP2 and **HTTP3**;\r\n- Unified middleware and handle interface;\r\n- Router can be nested infinitely, and multiple middlewares can be attached to any router;\r\n- Integrated Multipart form processing;\r\n- Support WebSocket, WebTransport;\r\n- Support OpenAPI, generate OpenAPI data automatic;\r\n- Support Acme, automatically get TLS certificate from [let's encrypt](https://letsencrypt.org/);\r\n- Support Tower Service and Layer;\r\n\r\n## ⚡️ Quick Start\r\n\r\nYou can view samples [here](https://github.com/salvo-rs/salvo/tree/main/examples), or view [official website](https://salvo.rs).\r\n\r\n### Hello World with ACME and HTTP3\r\n\r\n**It only takes a few lines of code to implement a server that supports ACME to automatically obtain certificates, and it\r\nsupports HTTP1, HTTP2, and HTTP3 protocols.**\r\n\r\n```rust\r\nuse salvo::prelude::*;\r\n\r\n#[handler]\r\nasync fn hello(res: \u0026mut Response) {\r\n    res.render(Text::Plain(\"Hello World\"));\r\n}\r\n\r\n#[tokio::main]\r\nasync fn main() {\r\n    let mut router = Router::new().get(hello);\r\n    let listener = TcpListener::new(\"0.0.0.0:443\")\r\n        .acme()\r\n        .add_domain(\"test.salvo.rs\") // Replace this domain name with your own.\r\n        .http01_challenge(\u0026mut router).quinn(\"0.0.0.0:443\");\r\n    let acceptor = listener.join(TcpListener::new(\"0.0.0.0:80\")).bind().await;\r\n    Server::new(acceptor).serve(router).await;\r\n}\r\n```\r\n\r\n### Middleware\r\n\r\nThere is no difference between a Handler and a Middleware, A Middleware is just a Handler. **You can write middleware\r\nwithout knowing concepts like associated types and generic types. If you can write a function, then you can write middleware!!!**\r\n\r\n```rust\r\nuse salvo::http::header::{self, HeaderValue};\r\nuse salvo::prelude::*;\r\n\r\n#[handler]\r\nasync fn add_header(res: \u0026mut Response) {\r\n    res.headers_mut()\r\n        .insert(header::SERVER, HeaderValue::from_static(\"Salvo\"));\r\n}\r\n```\r\n\r\nThen add it to router:\r\n\r\n```rust\r\nRouter::new().hoop(add_header).get(hello)\r\n```\r\n\r\nThis is a very simple middleware, it adds a `Header` to the `Response`, view [full source code](https://github.com/salvo-rs/salvo/blob/main/examples/middleware-add-header/src/main.rs).\r\n\r\n### Chainable tree routing system\r\n\r\nNormally we write routing like this:\r\n\r\n```rust\r\nRouter::with_path(\"articles\").get(list_articles).post(create_article);\r\nRouter::with_path(\"articles/{id}\")\r\n    .get(show_article)\r\n    .patch(edit_article)\r\n    .delete(delete_article);\r\n```\r\n\r\nOften, something like viewing articles and article lists does not require user login, but creating, editing, deleting articles, etc. require user login authentication permissions. The tree-like routing system in Salvo can meet this demand. We can write routers without user login together:\r\n\r\n```rust\r\nRouter::with_path(\"articles\")\r\n    .get(list_articles)\r\n    .push(Router::with_path(\"{id}\").get(show_article));\r\n```\r\n\r\nThen write the routers that require the user to login together, and use the corresponding middleware to verify whether the user is logged in:\r\n\r\n```rust\r\nRouter::with_path(\"articles\")\r\n    .hoop(auth_check)\r\n    .push(Router::with_path(\"{id}\").patch(edit_article).delete(delete_article));\r\n```\r\n\r\nAlthough these two routes have the same\r\n`path(\"articles\")`, they can still be added to the same parent route at the same time, so the final route looks like this:\r\n\r\n```rust\r\nRouter::new()\r\n    .push(\r\n        Router::with_path(\"articles\")\r\n            .get(list_articles)\r\n            .push(Router::with_path(\"{id}\").get(show_article)),\r\n    )\r\n    .push(\r\n        Router::with_path(\"articles\")\r\n            .hoop(auth_check)\r\n            .push(Router::with_path(\"{id}\").patch(edit_article).delete(delete_article)),\r\n    );\r\n```\r\n\r\n`{id}` matches a fragment in the path, under normal circumstances, the article`id` is just a number, which we can use regular expressions to restrict `id` matching rules, `r\"{id|\\d+}\"`.\r\n\r\nYou can also use `{**}`,  `{*+}` or`{*?}` to match all remaining path fragments.\r\nIn order to make the code more readable, you can also add appropriate name to make the path semantics more clear, for example: `{**file_path}`.\r\n\r\nSome regular expressions for matching paths need to be used frequently, and it can be registered in advance, such as GUID:\r\n\r\n```rust\r\nPathFilter::register_wisp_regex(\r\n    \"guid\",\r\n    Regex::new(\"[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}\").unwrap(),\r\n);\r\n```\r\n\r\nThis makes it more concise when path matching is required:\r\n\r\n```rust\r\nRouter::with_path(\"{id:guid}\").get(index)\r\n```\r\n\r\nView [full source code](https://github.com/salvo-rs/salvo/blob/main/examples/routing-guid/src/main.rs)\r\n\r\n### File upload\r\n\r\nWe can get file async by the function `file` in `Request`:\r\n\r\n```rust\r\n#[handler]\r\nasync fn upload(req: \u0026mut Request, res: \u0026mut Response) {\r\n    let file = req.file(\"file\").await;\r\n    if let Some(file) = file {\r\n        let dest = format!(\"temp/{}\", file.name().unwrap_or_else(|| \"file\".into()));\r\n        if let Err(e) = tokio::fs::copy(\u0026file.path, Path::new(\u0026dest)).await {\r\n            res.status_code(StatusCode::INTERNAL_SERVER_ERROR);\r\n        } else {\r\n            res.render(\"Ok\");\r\n        }\r\n    } else {\r\n        res.status_code(StatusCode::BAD_REQUEST);\r\n    }\r\n}\r\n```\r\n\r\n### Extract data from request\r\n\r\nYou can easily get data from multiple different data sources and assemble it into the type you want. You can define a custom type first, for example:\r\n\r\n```rust\r\n#[derive(Serialize, Deserialize, Extractible, Debug)]\r\n/// Get the data field value from the body by default.\r\n#[salvo(extract(default_source(from = \"body\")))]\r\nstruct GoodMan\u003c'a\u003e {\r\n    /// The id number is obtained from the request path parameter, and the data is automatically parsed as i64 type.\r\n    #[salvo(extract(source(from = \"param\")))]\r\n    id: i64,\r\n    /// Reference types can be used to avoid memory copying.\r\n    username: \u0026'a str,\r\n    first_name: String,\r\n    last_name: String,\r\n}\r\n```\r\n\r\nThen in `Handler` you can get the data like this:\r\n\r\n```rust\r\n#[handler]\r\nasync fn edit(req: \u0026mut Request) {\r\n    let good_man: GoodMan\u003c'_\u003e = req.extract().await.unwrap();\r\n}\r\n```\r\n\r\nYou can even pass the type directly to the function as a parameter, like this:\r\n\r\n```rust\r\n#[handler]\r\nasync fn edit\u003c'a\u003e(good_man: GoodMan\u003c'a\u003e) {\r\n    res.render(Json(good_man));\r\n}\r\n```\r\n\r\nView [full source code](https://github.com/salvo-rs/salvo/blob/main/examples/extract-nested/src/main.rs)\r\n\r\n### OpenAPI Supported\r\n\r\nPerfect support for OpenAPI can be achieved without making significant changes to the project.\r\n\r\n```rust\r\n#[derive(Serialize, Deserialize, ToSchema, Debug)]\r\nstruct MyObject\u003cT: ToSchema + std::fmt::Debug\u003e {\r\n    value: T,\r\n}\r\n\r\n#[endpoint]\r\nasync fn use_string(body: JsonBody\u003cMyObject\u003cString\u003e\u003e) -\u003e String {\r\n    format!(\"{:?}\", body)\r\n}\r\n#[endpoint]\r\nasync fn use_i32(body: JsonBody\u003cMyObject\u003ci32\u003e\u003e) -\u003e String {\r\n    format!(\"{:?}\", body)\r\n}\r\n#[endpoint]\r\nasync fn use_u64(body: JsonBody\u003cMyObject\u003cu64\u003e\u003e) -\u003e String {\r\n    format!(\"{:?}\", body)\r\n}\r\n\r\n#[tokio::main]\r\nasync fn main() {\r\n    tracing_subscriber::fmt().init();\r\n\r\n    let router = Router::new()\r\n        .push(Router::with_path(\"i32\").post(use_i32))\r\n        .push(Router::with_path(\"u64\").post(use_u64))\r\n        .push(Router::with_path(\"string\").post(use_string));\r\n\r\n    let doc = OpenApi::new(\"test api\", \"0.0.1\").merge_router(\u0026router);\r\n\r\n    let router = router\r\n        .push(doc.into_router(\"/api-doc/openapi.json\"))\r\n        .push(SwaggerUi::new(\"/api-doc/openapi.json\").into_router(\"swagger-ui\"));\r\n\r\n    let acceptor = TcpListener::new(\"127.0.0.1:5800\").bind().await;\r\n    Server::new(acceptor).serve(router).await;\r\n}\r\n```\r\n\r\n### 🛠️ Salvo CLI\r\n\r\nSalvo CLI is a command-line tool that simplifies the creation of new Salvo projects, supporting templates for web APIs, websites, databases (including SQLite, PostgreSQL, and MySQL via SQLx, SeaORM, Diesel, Rbatis), and basic middleware.\r\nYou can use [salvo-cli](https://github.com/salvo-rs/salvo-cli) to create a new Salvo project:\r\n\r\n#### install\r\n\r\n```bash\r\ncargo install salvo-cli\r\n```\r\n\r\n#### create a new Salvo project\r\n\r\n```bash\r\nsalvo new project_name\r\n```\r\n\r\n___\r\n\r\n### More Examples\r\n\r\nYou can find more examples in [examples](./examples/) folder. You can run these examples with the following command:\r\n\r\n```bash\r\ncd examples\r\ncargo run --bin example-basic-auth\r\n```\r\n\r\nYou can use any example name you want to run instead of `basic-auth` here.\r\n\r\n## 🚀 Performance\r\n\r\nBenchmark testing result can be found from here:\r\n\r\n[https://web-frameworks-benchmark.netlify.app/result?l=rust](https://web-frameworks-benchmark.netlify.app/result?l=rust)\r\n\r\n[https://www.techempower.com/benchmarks/#section=data-r22](https://www.techempower.com/benchmarks/#section=data-r22)\r\n\r\n## 🩸 Contributors\r\n\r\n\u003ca href=\"https://github.com/salvo-rs/salvo/graphs/contributors\"\u003e\r\n  \u003cimg src=\"https://contrib.rocks/image?repo=salvo-rs/salvo\" /\u003e\r\n\u003c/a\u003e\r\n\r\n## ☕ Donate\r\n\r\nSalvo is an open source project. If you want to support Salvo, you can ☕ [**buy me a coffee here**](https://ko-fi.com/chrislearn).\r\n\r\n## ⚠️ License\r\n\r\nSalvo is licensed under either of\r\n\r\n- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)).\r\n\r\n- MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)).\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsalvo-rs%2Fsalvo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsalvo-rs%2Fsalvo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsalvo-rs%2Fsalvo/lists"}