{"id":17249293,"url":"https://github.com/leon3s/ntex-rest-api-example","last_synced_at":"2025-07-05T14:35:24.705Z","repository":{"id":169772849,"uuid":"645789029","full_name":"leon3s/ntex-rest-api-example","owner":"leon3s","description":"Example on how to create a REST API in Rust with ntex and utoipa","archived":false,"fork":false,"pushed_at":"2023-05-26T15:04:56.000Z","size":206,"stargazers_count":29,"open_issues_count":0,"forks_count":2,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-14T05:08:28.540Z","etag":null,"topics":["learn-to-code","learning-resources","ntex","openapi","rust","swagger","tutorial"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/leon3s.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-05-26T12:43:32.000Z","updated_at":"2025-04-14T02:48:36.000Z","dependencies_parsed_at":null,"dependency_job_id":"e3fe304e-5684-45c5-93c8-60185ffc0ff6","html_url":"https://github.com/leon3s/ntex-rest-api-example","commit_stats":null,"previous_names":["leon3s/ntex-rest-api-example","leo-vernisse/ntex-rest-api-example"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leon3s%2Fntex-rest-api-example","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leon3s%2Fntex-rest-api-example/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leon3s%2Fntex-rest-api-example/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leon3s%2Fntex-rest-api-example/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/leon3s","download_url":"https://codeload.github.com/leon3s/ntex-rest-api-example/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248824682,"owners_count":21167345,"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":["learn-to-code","learning-resources","ntex","openapi","rust","swagger","tutorial"],"created_at":"2024-10-15T06:43:57.773Z","updated_at":"2025-04-14T05:08:37.714Z","avatar_url":"https://github.com/leon3s.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"![background](/background.png)\n\nHey, today I wanted to share my knowledge on how to write a Rest API in Rust. It may be easier than you think!\nWe won't showcase database connectivity in this article. Instead, we focused on demonstrating how to generate `OpenAPI` specifications and serve a `Swagger UI`.\n\nYou can find the full code source on [github](https://github.com/leon3s/ntex-rest-api-example).\n\nBefore starting, make sure you have [Rust](https://www.rust-lang.org) installed.\n\nLet's start by initializing a new project using `cargo init`.\n\n```sh\ncargo init my-rest-api\ncd my-rest-api\n```\n\nThat should produce the following directory structure:\n\n```console\n├── Cargo.toml\n└── src\n    └── main.rs\n```\n\nYou can use `rustfmt` for formatting. To do so, create a `rustfmt.toml` file with the following content:\n\n```toml\nindent_style = \"Block\"\nmax_width = 80\ntab_spaces = 2\nreorder_imports = false\nreorder_modules = false\nforce_multiline_blocks = true\nbrace_style = \"PreferSameLine\"\ncontrol_brace_style = \"AlwaysSameLine\"\n```\n\nI personally use VSCode. Optionally, you can add this configuration in your `.vscode/settings.json`:\n\n```json\n{\n  \"editor.rulers\": [80],\n  \"editor.tabSize\": 2,\n  \"editor.detectIndentation\": false,\n  \"editor.trimAutoWhitespace\": true,\n  \"editor.formatOnSave\": true,\n  \"files.insertFinalNewline\": true,\n  \"files.trimTrailingWhitespace\": true,\n  \"rust-analyzer.showUnlinkedFileNotification\": false,\n  \"rust-analyzer.checkOnSave\": true,\n  \"rust-analyzer.check.command\": \"clippy\"\n}\n```\n\nYour new directory structure should look like this:\n\n```console\n├── .gitignore\n├── .vscode\n│   └── settings.json\n├── Cargo.lock\n├── Cargo.toml\n├── rustfmt.toml\n└── src\n    └── main.rs\n```\n\nWe are going to use [ntex](https://ntex.rs) as our HTTP framework.\u003cbr/\u003e\nWe can install Rust dependencies by running `cargo add`.\u003cbr/\u003e\nNote that when using `ntex`, we have the ability to choose our `runtime`.\u003cbr/\u003e\nTo quickly summarize, the `runtime` will manage your `async|await` pattern.\u003cbr/\u003e\nIf you are familiar with the `nodejs runtime`, it's kind of similar in usage.\n\nFor this tutorial, we are going to use tokio as it seems to be the more popular choice. Let's add ntex as a dependency:\n\n```sh\ncargo add ntex --features tokio\n```\n\nThen we are going to update our `main.rs` file with the following content:\n\n```rust\nuse ntex::web;\n\n#[web::get(\"/\")]\nasync fn index() -\u003e \u0026'static str {\n  \"Hello world!\"\n}\n\n#[ntex::main]\nasync fn main() -\u003e std::io::Result\u003c()\u003e {\n  web::server(|| web::App::new().service(index))\n    .bind((\"0.0.0.0\", 8080))?\n    .run()\n    .await?;\n  Ok(())\n}\n```\n\nWe can run our project by using the following command:\n\n```sh\ncargo run\n```\n\nThis command will compile our code and run it.\u003cbr/\u003e\nYou should see the following output:\n\n```console\nFinished dev [unoptimized + debuginfo] target(s) in 17.38s\nRunning `target/debug/my-rest-api`\n```\n\nWe can test our server using curl:\n\n```\ncurl -v localhost:8080\n```\n\n```console\n*   Trying 127.0.0.1:8080...\n* TCP_NODELAY set\n* Connected to localhost (127.0.0.1) port 8080 (#0)\n\u003e GET / HTTP/1.1\n\u003e Host: localhost:8080\n\u003e User-Agent: curl/7.68.0\n\u003e Accept: */*\n\u003e\n* Mark bundle as not supporting multiuse\n\u003c HTTP/1.1 200 OK\n\u003c content-length: 12\n\u003c content-type: text/plain; charset=utf-8\n\u003c date: Fri, 26 May 2023 11:43:01 GMT\n\u003c\n* Connection #0 to host localhost left intact\nHello world!%\n```\n\nCongratulations! You now have your first HTTP server in `Rust`!\n\nNow let's create our first `REST endpoints`.\n\nRegarding the directory architecture, it's up to personal preference. In `ntex`, we use the `.service()` method to add new `endpoints`. Therefore, I have chosen to create a directory called `services` to house my endpoints.\n\nLet's create the directory:\n\n```sh\nmkdir src/services\ntouch src/services/mod.rs\n```\n\nNote that by default, `Rust` tries to import a `mod.rs` file from our directories.\n\nLet's create our default `endpoints` inside `services/mod.rs`:\n\n```rust\nuse ntex::web;\n\npub async fn default() -\u003e web::HttpResponse {\n  web::HttpResponse::NotFound().finish()\n}\n```\n\nNow we need to indicate that we want to use this module in our main.rs:\n\n```rust\nuse ntex::web;\n\nmod services;\n\n#[ntex::main]\nasync fn main() -\u003e std::io::Result\u003c()\u003e {\n  web::server(|| {\n    web::App::new()\n      // Default endpoint for unregisterd endpoints\n      .default_service(web::route().to(services::default)\n    )\n  })\n  .bind((\"0.0.0.0\", 8080))?\n  .run()\n  .await?;\n  Ok(())\n}\n```\n\nNow, for any unregistered `endpoints`, we will have a 404 error.\n\nBefore continuing, let's add four dependencies: `serde` and `serde_json` for JSON serialization, and `utoipa` with `utoipa-swagger-ui` to have an `OpenAPI` swagger.\n\n```sh\ncargo add serde --features derive\ncargo add serde_json utoipa utoipa-swagger-ui\n```\n\nNext, we are going to create our own `HttpError` type as helpers. Create a file under `src/error.rs` with the following content:\n\n```rust\nuse ntex::web;\nuse ntex::http;\nuse utoipa::ToSchema;\nuse serde::{Serialize, Deserialize};\n\n/// An http error response\n#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]\npub struct HttpError {\n  /// The error message\n  pub msg: String,\n  /// The http status code, skipped in serialization\n  #[serde(skip)]\n  pub status: http::StatusCode,\n}\n\n/// Helper function to display an HttpError\nimpl std::fmt::Display for HttpError {\n  fn fmt(\u0026self, f: \u0026mut std::fmt::Formatter\u003c'_\u003e) -\u003e std::fmt::Result {\n    write!(f, \"[{}] {}\", self.status, self.msg)\n  }\n}\n\n/// Implement standard error for HttpError\nimpl std::error::Error for HttpError {}\n\n/// Helper function to convert an HttpError into a ntex::web::HttpResponse\nimpl web::WebResponseError for HttpError {\n  fn error_response(\u0026self, _: \u0026web::HttpRequest) -\u003e web::HttpResponse {\n    web::HttpResponse::build(self.status).json(\u0026self)\n  }\n}\n```\n\nWe need to import our error module in our `main.rs` let update it:\n\n```rust\nuse ntex::web;\n\nmod error;\nmod services;\n\n#[ntex::main]\nasync fn main() -\u003e std::io::Result\u003c()\u003e {\n  web::server(|| {\n    web::App::new()\n      // Default endpoint for unregisterd endpoints\n      .default_service(web::route().to(services::default)\n    )\n  })\n  .bind((\"0.0.0.0\", 8080))?\n  .run()\n  .await?;\n  Ok(())\n}\n```\n\nI think we are ready to write some example `endpoints`. Let's simulate a todo list and create a new file under `src/services/todo.rs`:\n\n```rust\nuse ntex::web;\n\n#[web::get(\"/todos\")]\npub async fn get_todos() -\u003e web::HttpResponse {\n  web::HttpResponse::Ok().finish()\n}\n\n#[web::post(\"/todos\")]\npub async fn create_todo() -\u003e web::HttpResponse {\n  web::HttpResponse::Created().finish()\n}\n\n#[web::get(\"/todos/{id}\")]\npub async fn get_todo() -\u003e web::HttpResponse {\n  web::HttpResponse::Ok().finish()\n}\n\n#[web::put(\"/todos/{id}\")]\npub async fn update_todo() -\u003e web::HttpResponse {\n  web::HttpResponse::Ok().finish()\n}\n\n#[web::delete(\"/todos/{id}\")]\npub async fn delete_todo() -\u003e web::HttpResponse {\n  web::HttpResponse::Ok().finish()\n}\n\npub fn ntex_config(cfg: \u0026mut web::ServiceConfig) {\n  cfg.service(get_todos);\n  cfg.service(create_todo);\n  cfg.service(get_todo);\n  cfg.service(update_todo);\n  cfg.service(delete_todo);\n}\n```\n\nWe need to update our `src/services/mod.rs` to import our `todo.rs`:\n\n```rust\npub mod todo;\n\nuse ntex::web;\n\npub async fn default() -\u003e web::HttpResponse {\n  web::HttpResponse::NotFound().finish()\n}\n```\n\nIn our `main.rs`:\n\n```rust\nuse ntex::web;\n\nmod error;\nmod services;\n\n#[ntex::main]\nasync fn main() -\u003e std::io::Result\u003c()\u003e {\n  web::server(|| {\n    web::App::new()\n      // Register todo endpoints\n      .configure(services::todo::ntex_config)\n      // Default endpoint for unregisterd endpoints\n      .default_service(web::route().to(services::default))\n  })\n  .bind((\"0.0.0.0\", 8080))?\n  .run()\n  .await?;\n  Ok(())\n}\n```\n\nLet's create some data structure for our `Todo`.\nWe are going to create a new directory `src/models` with his `mod.rs` and a `todo.rs`\n\n```sh\nmkdir src/models\ntouch src/models/mod.rs\ntouch src/models/todo.rs\n```\n\nIn our `src/models/mod.rs` we are going to import `todo.rs`:\n\n```rust\npub mod todo;\n```\n\nAnd inside `src/models/todo.rs`, we are going to add some `data structure`:\n\n```rust\nuse utoipa::ToSchema;\nuse serde::{Serialize, Deserialize};\n\n/// Todo model\n#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]\npub struct Todo {\n  /// The todo id\n  pub id: i32,\n  /// The todo title\n  pub title: String,\n  /// The todo completed status\n  pub completed: bool,\n}\n\n/// Partial Todo model\n#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]\npub struct TodoPartial {\n  /// The todo title\n  pub title: String,\n}\n```\n\nYou may notice that we use the `serde` and `utoipa` derive macros to enable `JSON` serialization and conversion to `OpenAPI Schema`.\n\nDon't forget to update your `main.rs` to import our `models`:\n\n```rust\nuse ntex::web;\n\nmod error;\nmod models;\nmod services;\n\n#[ntex::main]\nasync fn main() -\u003e std::io::Result\u003c()\u003e {\n  web::server(|| {\n    web::App::new()\n      // Register todo endpoints\n      .configure(services::todo::ntex_config)\n      // Default endpoint for unregisterd endpoints\n      .default_service(web::route().to(services::default))\n  })\n  .bind((\"0.0.0.0\", 8080))?\n  .run()\n  .await?;\n  Ok(())\n}\n```\n\nWith the models in place, we can now generate type-safe endpoints with their documentation. Let's update our `endpoints` inside `src/services/todo.rs`:\n\n```rust\nuse ntex::web;\n\nuse crate::models::todo::TodoPartial;\n\n/// List all todos\n#[utoipa::path(\n  get,\n  path = \"/todos\",\n  responses(\n    (status = 200, description = \"List of Todo\", body = [Todo]),\n  ),\n)]\n#[web::get(\"/todos\")]\npub async fn get_todos() -\u003e web::HttpResponse {\n  web::HttpResponse::Ok().finish()\n}\n\n/// Create a new todo\n#[utoipa::path(\n  post,\n  path = \"/todos\",\n  request_body = TodoPartial,\n  responses(\n    (status = 201, description = \"Todo created\", body = Todo),\n  ),\n)]\n#[web::post(\"/todos\")]\npub async fn create_todo(\n  _todo: web::types::Json\u003cTodoPartial\u003e,\n) -\u003e web::HttpResponse {\n  web::HttpResponse::Created().finish()\n}\n\n/// Get a todo by id\n#[utoipa::path(\n  get,\n  path = \"/todos/{id}\",\n  responses(\n    (status = 200, description = \"Todo found\", body = Todo),\n    (status = 404, description = \"Todo not found\", body = HttpError),\n  ),\n)]\n#[web::get(\"/todos/{id}\")]\npub async fn get_todo() -\u003e web::HttpResponse {\n  web::HttpResponse::Ok().finish()\n}\n\n/// Update a todo by id\n#[utoipa::path(\n  put,\n  path = \"/todos/{id}\",\n  request_body = TodoPartial,\n  responses(\n    (status = 200, description = \"Todo updated\", body = Todo),\n    (status = 404, description = \"Todo not found\", body = HttpError),\n  ),\n)]\n#[web::put(\"/todos/{id}\")]\npub async fn update_todo() -\u003e web::HttpResponse {\n  web::HttpResponse::Ok().finish()\n}\n\n/// Delete a todo by id\n#[utoipa::path(\n  delete,\n  path = \"/todos/{id}\",\n  responses(\n    (status = 200, description = \"Todo deleted\", body = Todo),\n    (status = 404, description = \"Todo not found\", body = HttpError),\n  ),\n)]\n#[web::delete(\"/todos/{id}\")]\npub async fn delete_todo() -\u003e web::HttpResponse {\n  web::HttpResponse::Ok().finish()\n}\n\npub fn ntex_config(cfg: \u0026mut web::ServiceConfig) {\n  cfg.service(get_todos);\n  cfg.service(create_todo);\n  cfg.service(get_todo);\n  cfg.service(update_todo);\n  cfg.service(delete_todo);\n}\n```\n\nWith utoipa, we will be able to serve our Swagger documentation.\n\nLet's create a new file under `src/services/openapi.rs`:\n\n```rust\nuse std::sync::Arc;\n\nuse ntex::web;\nuse ntex::http;\nuse ntex::util::Bytes;\nuse utoipa::OpenApi;\n\nuse crate::error::HttpError;\nuse crate::models::todo::{Todo, TodoPartial};\n\nuse super::todo;\n\n/// Main structure to generate OpenAPI documentation\n#[derive(OpenApi)]\n#[openapi(\n  paths(\n    todo::get_todos,\n    todo::create_todo,\n    todo::get_todo,\n    todo::update_todo,\n    todo::delete_todo,\n  ),\n  components(schemas(Todo, TodoPartial, HttpError))\n)]\npub(crate) struct ApiDoc;\n\n#[web::get(\"/{tail}*\")]\nasync fn get_swagger(\n  tail: web::types::Path\u003cString\u003e,\n  openapi_conf: web::types::State\u003cArc\u003cutoipa_swagger_ui::Config\u003c'static\u003e\u003e\u003e,\n) -\u003e Result\u003cweb::HttpResponse, HttpError\u003e {\n  if tail.as_ref() == \"swagger.json\" {\n    let spec = ApiDoc::openapi().to_json().map_err(|err| HttpError {\n      status: http::StatusCode::INTERNAL_SERVER_ERROR,\n      msg: format!(\"Error generating OpenAPI spec: {}\", err),\n    })?;\n    return Ok(\n      web::HttpResponse::Ok()\n        .content_type(\"application/json\")\n        .body(spec),\n    );\n  }\n  let conf = openapi_conf.as_ref().clone();\n  match utoipa_swagger_ui::serve(\u0026tail, conf.into()).map_err(|err| {\n    HttpError {\n      msg: format!(\"Error serving Swagger UI: {}\", err),\n      status: http::StatusCode::INTERNAL_SERVER_ERROR,\n    }\n  })? {\n    None =\u003e Err(HttpError {\n      status: http::StatusCode::NOT_FOUND,\n      msg: format!(\"path not found: {}\", tail),\n    }),\n    Some(file) =\u003e Ok({\n      let bytes = Bytes::from(file.bytes.to_vec());\n      web::HttpResponse::Ok()\n        .content_type(file.content_type)\n        .body(bytes)\n    }),\n  }\n}\n\npub fn ntex_config(config: \u0026mut web::ServiceConfig) {\n  let swagger_config = Arc::new(\n    utoipa_swagger_ui::Config::new([\"/explorer/swagger.json\"])\n      .use_base_layout(),\n  );\n  config.service(\n    web::scope(\"/explorer/\")\n      .state(swagger_config)\n      .service(get_swagger),\n  );\n}\n```\n\nDon't forget to update `src/services/mod.rs` to import `src/services/openapi.rs`:\n\n```rust\npub mod todo;\npub mod openapi;\n\nuse ntex::web;\n\npub async fn default() -\u003e web::HttpResponse {\n  web::HttpResponse::NotFound().finish()\n}\n```\n\nThen we can update our `main.rs` to register our explorer endpoints:\n\n```rust\nuse ntex::web;\n\nmod error;\nmod models;\nmod services;\n\n#[ntex::main]\nasync fn main() -\u003e std::io::Result\u003c()\u003e {\n  web::server(|| {\n    web::App::new()\n      // Register swagger endpoints\n      .configure(services::openapi::ntex_config)\n      // Register todo endpoints\n      .configure(services::todo::ntex_config)\n      // Default endpoint for unregisterd endpoints\n      .default_service(web::route().to(services::default))\n  })\n  .bind((\"0.0.0.0\", 8080))?\n  .run()\n  .await?;\n  Ok(())\n}\n```\n\nWe are good to go. Let's run our server:\n\n```sh\ncargo run\n```\n\nThen we should be able to access to our [explorer](http://localhost:8080/explorer/) on [http://localhost:8080/explorer/](http://localhost:8080/explorer/)\n\n![swagger](/swagger.png)\n\nI Hope you will try to write your next REST API in Rust !\n\nDon't forget to take a look at the dependencies documentation:\n\n- [ntex](https://ntex.rs)\n- [serde](https://serde.rs)\n- [serde_json](https://github.com/serde-rs/json)\n- [utoipa](https://github.com/juhaku/utoipa)\n\n## Bonus\n\nCreate a production docker image !\nAdd a `Dockerfile` in your project directory with the following content:\n\n```Dockerfile\n# Builder\nFROM rust:1.69.0-alpine3.17 as builder\n\nWORKDIR /app\n\n## Install build dependencies\nRUN apk add alpine-sdk musl-dev build-base upx\n\n## Copy source code\nCOPY Cargo.toml Cargo.lock ./\nCOPY src ./src\n\n## Build release binary\nRUN cargo build --release --target x86_64-unknown-linux-musl\n## Pack release binary with UPX (optional)\nRUN upx --best --lzma /app/target/x86_64-unknown-linux-musl/release/my-rest-api\n\n# Runtime\nFROM scratch\n\n## Copy release binary from builder\nCOPY --from=builder /app/target/x86_64-unknown-linux-musl/release/my-rest-api /app\n\nENTRYPOINT [\"/app\"]\n```\n\nOptionally you can add this `release` profile in your `Cargo.toml`:\n\n```toml\n[profile.release]\nopt-level = \"z\"\ncodegen-units = 1\nstrip = true\nlto = true\n```\n\nThis will optimise the release binary to be as small as possible. Additionally with [upx](https://upx.github.io/) we can create really small docker image !\n\nBuild your image:\n\n```sh\ndocker build -t my-rest-api:0.0.1 -f Dockerfile .\n```\n\n![docker_image_ls](/docker_image_ls.png)\n\nIf you want to see a more real world usecase i invite you to take a look at my opensource project [Nanocl](https://github.com/nxthat/nanocl). That try to simplify the development and deployment of micro services, with containers or virtual machines !\n\nHappy coding !\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleon3s%2Fntex-rest-api-example","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fleon3s%2Fntex-rest-api-example","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleon3s%2Fntex-rest-api-example/lists"}