DEV Community

Cover image for How to Test the Rust API?
Hassann
Hassann

Posted on • Originally published at apidog.com

How to Test the Rust API?

Rust gives you a fast, type-safe HTTP server in a few hundred lines. What it does not give you is a fast feedback loop for testing that server. The compile cycle is long, cargo test reruns broad test sets after small trait changes, and most Rust HTTP frameworks push you toward writing integration tests before you have manually exercised the endpoint. If you want to ship an API and not just a binary, test the running server over HTTP.

Try Apidog today

This guide walks through a Rust API testing workflow in Apidog: connect Apidog to your Axum or Actix server, build requests, validate Serde JSON, handle JWT auth, mock unfinished endpoints, and package everything as a CI test scenario.

If you are coming from Postman or curl, Apidog also gives you design-first features: OpenAPI output from saved requests, shareable mock URLs, and team environments. For migration details, read the separate Postman migration story. This post stays focused on Rust.

TL;DR

  • Run your Rust server locally with cargo run, add http://localhost:3000 as an Apidog environment, and store secrets as variables.
  • Build a first request for GET /healthz, save it, and reuse the same base URL and auth across the folder.
  • For JSON endpoints, send payloads that match your Serde structs and add response assertions in the Tests tab.
  • For protected routes, mint a JWT once, save it as {{token}}, and apply Bearer auth at the folder level.
  • Mock unfinished Rust handlers in Apidog so frontend work can continue before the handler is complete.
  • Save the request set as a Test Scenario and run it in CI with apidog-cli.

Why test a Rust API outside the Rust toolchain?

cargo test is useful, but it is code-centric. To verify HTTP behavior, you usually write framework-specific tests around status codes, headers, payloads, and error cases.

Apidog adds a contract layer on top of the running server:

  1. Contract checks run against the binary. You test real HTTP behavior without waiting on Rust-only test code.
  2. Mocks are shareable. Frontend developers can use a stable URL that returns the agreed JSON shape.
  3. OpenAPI output stays close to reality. Saved requests can become an OpenAPI 3.1 document for typed clients and API consumers.

Step 1: Add your Rust server as an Apidog environment

Start your Rust API. For an Axum project, a minimal health check looks like this:

use axum::{routing::get, Router};
use tokio::net::TcpListener;

#[tokio::main]
async fn main() {
    let app = Router::new().route("/healthz", get(|| async { "ok" }));
    let listener = TcpListener::bind("0.0.0.0:3000").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}
Enter fullscreen mode Exit fullscreen mode

In Apidog:

  1. Create a new project.
  2. Open Environment Management from the top-right dropdown.
  3. Add an environment named Rust Local.
Variable Value
baseUrl http://localhost:3000
token leave empty for now
apiVersion v1

Add a second environment named Rust Staging with your deployed base URL. Apidog scopes variables per environment, so switching from local to staging only requires changing the selected environment.

Step 2: Hit the first endpoint

Create a folder named Rust API, then add a request:

  • Method: GET
  • URL: {{baseUrl}}/healthz

Send the request. If the server is running, you should receive:

  • Status: 200
  • Body: ok

Save the request as health-check.

If you get connection refused, check the bind address and port. For local development, bind to 0.0.0.0:3000 so Apidog and Docker-based tools can reach the server consistently.

Step 3: Test JSON request and response with Serde

Most Rust APIs use Serde-backed JSON handlers. Add a POST /users route:

use axum::{extract::Json, routing::post, Router};
use serde::{Deserialize, Serialize};

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

#[derive(Serialize)]
struct User {
    id: u64,
    name: String,
    email: String,
}

async fn create_user(Json(payload): Json<CreateUser>) -> Json<User> {
    Json(User {
        id: 1,
        name: payload.name,
        email: payload.email,
    })
}

let app = Router::new().route("/users", post(create_user));
Enter fullscreen mode Exit fullscreen mode

In Apidog, create a request:

  • Method: POST
  • URL: {{baseUrl}}/users
  • Body type: JSON
{
  "name": "Ada Lovelace",
  "email": "ada@example.com"
}
Enter fullscreen mode Exit fullscreen mode

Send the request and save it as create-user.

Now add assertions in the Tests tab:

pm.test("Status is 200", () => {
  pm.expect(pm.response.code).to.eql(200);
});

pm.test("Body has id, name, email", () => {
  const body = pm.response.json();

  pm.expect(body).to.have.property("id");
  pm.expect(body.name).to.eql("Ada Lovelace");
  pm.expect(body.email).to.match(/^[^@]+@[^@]+$/);
});
Enter fullscreen mode Exit fullscreen mode

This catches contract drift. For example, if a future Serde change renames a field, the HTTP response assertion fails even if Rust type-level tests still pass.

Step 4: Cover Serde rejection cases

Bad input is part of your API contract. Create requests that intentionally violate the schema:

Request Body Expected
create-user-missing-email { "name": "Ada" } 422, body mentions missing field email
create-user-extra-field { "name": "Ada", "email": "a@b.c", "admin": true } 200 if #[serde(deny_unknown_fields)] is absent; 422 otherwise
create-user-wrong-type { "name": 1, "email": "a@b.c" } 422, mentions invalid type: integer

Add a status-code assertion to each request. This documents your actual validation policy. If you later add #[serde(deny_unknown_fields)], the extra-field test will fail and signal a contract change.

Step 5: Test JWT-protected routes

Production Rust APIs often hide handlers behind auth middleware. A simplified Axum-style JWT flow might look like this:

use axum_extra::extract::cookie::PrivateCookieJar;
use jsonwebtoken::{decode, DecodingKey, Validation};

async fn me(jar: PrivateCookieJar) -> Result<Json<User>, StatusCode> {
    let token = jar.get("token").ok_or(StatusCode::UNAUTHORIZED)?;

    let claims = decode::<Claims>(
        token.value(),
        &DecodingKey::from_secret(b"secret"),
        &Validation::default(),
    )
    .map_err(|_| StatusCode::UNAUTHORIZED)?;

    Ok(Json(User {
        id: claims.claims.sub,
        name: "Ada".into(),
        email: "ada@example.com".into(),
    }))
}
Enter fullscreen mode Exit fullscreen mode

In Apidog, avoid manually generating a JWT for every run. Add a folder-level pre-request script:

const jwt = require("jsonwebtoken");

const token = jwt.sign(
  { sub: 1, exp: Math.floor(Date.now() / 1000) + 3600 },
  "secret"
);

pm.environment.set("token", token);
Enter fullscreen mode Exit fullscreen mode

Then configure folder auth:

  • Auth type: Bearer Token
  • Token value: {{token}}

Every request in the folder now inherits a fresh JWT. For more assertion examples, see how to test JWT authentication in APIs.

Step 6: Test streaming and Server-Sent Events

Rust web frameworks support streaming responses. In Axum, Sse wraps a futures::Stream and emits text/event-stream chunks.

The wire format is:

data: { ... }

Enter fullscreen mode Exit fullscreen mode

Create a normal GET request to the SSE endpoint. When Apidog sees Content-Type: text/event-stream, the response panel switches to streaming mode and displays frames as they arrive.

Useful checks for SSE endpoints:

  • The first chunk arrives within the latency you expect.
  • A specific event appears before the connection closes, such as event: done.
  • The stream completes within a bounded duration.
  • A request timeout is set so an infinite stream fails the test instead of hanging forever.

If your endpoint uses WebSockets instead of SSE, use Apidog’s WebSocket request type. Save the message sequence and assert on the responses.

Step 7: Mock the Rust API for parallel frontend development

Frontend work often blocks on endpoints that are not implemented yet. Apidog mocks let you publish a stable URL that returns the agreed response shape before the Rust handler is finished.

For the create-user request:

  1. Right-click the request.
  2. Choose Smart Mock.
  3. Enable the mock.

Apidog serves a synthetic User response at a URL like:

https://mock.apidog.com/m1/<projectId>/users
Enter fullscreen mode Exit fullscreen mode

The mock body matches your saved example, and the frontend can POST to it as if it were the real Rust server.

For dynamic mocks, switch to Advanced Mock and add a script:

return {
  id: Math.floor(Math.random() * 10000),
  name: body.name,
  email: body.email,
  createdAt: new Date().toISOString()
};
Enter fullscreen mode Exit fullscreen mode

Now the mock echoes the submitted name and email while generating an id and timestamp. When the Rust handler is ready, the frontend switches its base URL back to http://localhost:3000.

The same workflow applies across stacks. See Apidog’s guides on building and testing a Spring Boot API and the general API testing workflow.

Step 8: Save as a CI test scenario

Apidog Test Scenarios chain requests with shared variables and run them headlessly.

Create a scenario like this:

  1. Run health-check; assert 200.
  2. Run create-user; assert 200; capture body.id into a variable.
  3. Run create-user-missing-email; assert 422.
  4. Run me with the JWT pre-request script; assert 200 and verify the returned id.
  5. Run the SSE request; assert the stream completes within 5 seconds.

Export the scenario as JSON, commit it to your repo under tests/apidog/, and run it from CI:

- name: Run API contract tests
  run: |
    cargo build --release
    ./target/release/myserver &
    sleep 2
    apidog-cli run tests/apidog/contract.json --env "Rust Local"
Enter fullscreen mode Exit fullscreen mode

Every pull request that touches a handler now runs against a live Rust binary. If a Serde rename, status-code change, or JWT validation update breaks the public contract, CI catches it.

Step 9: Generate OpenAPI from saved requests

When the request set is stable:

  1. Open Apidog’s Export menu.
  2. Choose OpenAPI 3.1.
  3. Export the spec.

The generated document covers saved requests and includes request/response examples. API consumers can use it to generate typed clients for TypeScript, Swift, Kotlin, Python, and other languages.

If you want the spec checked into your Rust repo, run apidog-cli export from CI and write the output to openapi.json.

FAQ

Does Apidog work with both Axum and Actix-web?

Yes. Apidog talks HTTP, not Rust. Anything that responds to HTTP requests works the same way: Axum, Actix-web, Rocket, Warp, Poem, Loco, and others.

The main Rust-specific local testing detail is binding to 0.0.0.0 instead of 127.0.0.1 when other tools need to reach the server.

How do I test handlers that panic?

Run your server with tower-http’s CatchPanicLayer in front of the router. The panic becomes a 500 response. Build an Apidog request that triggers the panic path and assert the 500.

If you do not wrap panics, the connection may drop and Apidog reports a network error. That can also be a valid contract check if it reflects your intended behavior.

Can I run Apidog against a Rust binary in Docker?

Yes. Point baseUrl at the container’s exposed port.

If the container runs inside Docker Compose, either run the Apidog runner on the same network or use the host-mapped port.

What about gRPC?

Apidog has a gRPC request type. Import your .proto files, choose a service and method, fill in the payload, and send the request. Environments, auth, and test scenarios work with the same pattern.

Does the test scenario replace cargo test?

No. Keep Rust unit tests in Rust. Use Apidog for the running HTTP contract.

They catch different bugs:

  • Rust unit tests catch broken functions and internal logic.
  • Apidog tests catch broken response shapes, missing headers, auth failures, and status-code changes.

Use both.

Is Apidog free for Rust open-source projects?

Yes. The Apidog client is free for individuals and small teams. Test scenarios, mocks, and OpenAPI export are part of the free tier. If you maintain a public Rust API, you can include the project file in your repo so contributors can run the same test suite.

Wrap up

Rust APIs need a feedback loop that does not depend entirely on compiler and test cycles. An Apidog request collection gives you real HTTP checks, reusable assertions, shareable mocks, and CI scenarios that run against the live binary.

Download Apidog, point it at your Rust server, and turn your Axum or Actix handlers into a repeatable API contract test suite.

Top comments (0)