Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

5. Test, Ship, And Evolve

Strict API definitions are most useful when CI checks the important feature combinations and when tests assert that auth metadata stays visible in the generated specs.

Contract Tests

Test DTO serialization for wire stability:

#[test]
fn create_task_request_serializes_expected_shape() {
    let value = serde_json::to_value(CreateTaskRequest {
        title: "Write release notes".to_string(),
        assignee_id: None,
    })
    .unwrap();

    assert_eq!(
        value,
        serde_json::json!({
            "title": "Write release notes",
            "assignee_id": null
        })
    );
}

Test generated OpenAPI or OpenRPC output for route shape and permission metadata:

#[test]
fn openapi_documents_task_permissions() {
    let doc = workspace_api::generate_taskservice_openapi();
    let create = &doc["paths"]["/projects/{project_id}/tasks"]["post"];

    assert_eq!(create["security"][0]["bearerAuth"], serde_json::json!([]));
    assert_eq!(create["x-permissions"], serde_json::json!(["task:write"]));
}

These tests catch accidental auth changes before a client discovers them.

Server Tests

Use an in-memory Axum test server for generated routes:

#[tokio::test]
async fn create_task_requires_write_permission() {
    let app = build_app_with_test_auth();
    let server = axum_test::TestServer::new(app).unwrap();

    let response = server
        .post("/api/v1/projects/project-123/tasks")
        .authorization_bearer("read-only-token")
        .json(&CreateTaskRequest {
            title: "Write release notes".to_string(),
            assignee_id: None,
        })
        .await;

    response.assert_status_forbidden();
}

File-service tests should include oversized requests, missing required parts, wrong content types, and auth rejection before upload handling begins.

Feature Matrix

Add CI checks for the API crate itself:

cargo check -p workspace-api --no-default-features --locked
cargo check -p workspace-api --no-default-features --features server --locked
cargo check -p workspace-api --no-default-features --features client --locked
cargo check -p workspace-api --target wasm32-unknown-unknown --no-default-features --features client --locked

This proves DTO-only, server-only, native client, and browser client builds stay separate.

Deployment Shape

A typical release pipeline does three things:

  • build and test the Rust workspace;
  • generate OpenAPI/OpenRPC documents from the API crate;
  • publish generated docs and client inputs alongside the server artifact.

The mdBook in this repository is built in CI and published to GitHub Pages from the master branch. Application repositories can use the same pattern for project-specific API documentation.

Evolving The API

Prefer additive changes when possible:

  • add optional request fields instead of requiring new fields immediately;
  • add response fields that old clients can ignore;
  • add new operations before removing old operations;
  • preserve permission names unless the capability truly changed.

When a wire shape must change, use versioning support where the macro provides it. Keep the service implementation canonical and let the API boundary migrate legacy request and response shapes. That makes compatibility an explicit part of the contract instead of an untested handler branch.

Before removing a legacy operation, check generated specs, client usage, and server logs. The contract should tell you what still exists; telemetry should tell you what is still used.