# tonic-rest-build
[](https://crates.io/crates/tonic-rest-build)
[](https://docs.rs/tonic-rest-build)
[](LICENSE-MIT)
[](https://blog.rust-lang.org/2025/02/20/Rust-1.85.0.html)
Build-time REST codegen from protobuf `google.api.http` annotations for Tonic + Axum.
Part of the [tonic-rest](https://github.com/zs-dima/tonic-rest) ecosystem — define your API once in proto files, get gRPC, REST, and OpenAPI 3.1.
Reads a compiled proto `FileDescriptorSet`, extracts `google.api.http` annotations,
and generates Axum route handler code that calls through Tonic service traits — keeping
proto files as the single source of truth for both gRPC and REST APIs.
## Key Features
- **Proto as single source of truth** — one definition drives gRPC, REST endpoints, and OpenAPI docs
- **Build-time codegen** — Axum handlers generated from `FileDescriptorSet` at compile time; zero runtime overhead or reflection
- **Standard annotations** — uses [`google.api.http`](https://cloud.google.com/endpoints/docs/grpc/transcoding) bindings, not a proprietary DSL
- **Zero-config auto-discovery** — scans the descriptor set for any service with HTTP annotations; no manual package listing required
- **SSE for server streaming** — streaming RPCs are automatically exposed as Server-Sent Events endpoints
- **Serde auto-wiring** — `configure_prost_serde` discovers WKT fields and applies `#[serde(with)]` attributes automatically
## How It Works
Annotate your proto service with `google.api.http`:
```protobuf
service ItemService {
rpc CreateItem(CreateItemRequest) returns (Item) {
option (google.api.http) = { post: "/v1/items" body: "*" };
}
rpc GetItem(GetItemRequest) returns (Item) {
option (google.api.http) = { get: "/v1/items/{item_id}" };
}
}
```
The generated code calls through Tonic service traits — sharing auth, validation,
and business logic with gRPC handlers:
```rust,ignore
async fn rest_create_item<S: ItemService>(
State(service): State<Arc<S>>,
headers: HeaderMap,
Json(body): Json<CreateItemRequest>,
) -> Result<Json<Item>, RestError> {
let req = build_tonic_request::<_, ()>(body, &headers, None);
let response = service.create_item(req).await?;
Ok(Json(response.into_inner()))
}
```
## Quick Start
```toml
[dependencies]
tonic-rest = "0.1"
[build-dependencies]
tonic-rest-build = "0.1"
prost-build = "0.14"
```
Zero-config `build.rs` — auto-discovers packages from the descriptor set:
```rust,ignore
use tonic_rest_build::{RestCodegenConfig, generate, dump_file_descriptor_set};
const PROTO_FILES: &[&str] = &["proto/service.proto"];
const PROTO_INCLUDES: &[&str] = &["proto"];
fn main() {
let out_dir = std::env::var("OUT_DIR").unwrap();
let descriptor_path = format!("{out_dir}/file_descriptor_set.bin");
// Phase 1: Compile protos → descriptor set
let descriptor_bytes = dump_file_descriptor_set(PROTO_FILES, PROTO_INCLUDES, &descriptor_path);
// Phase 2: Compile protos → Rust (prost/tonic)
let mut config = prost_build::Config::new();
config.file_descriptor_set_path(&descriptor_path);
config.compile_protos(PROTO_FILES, PROTO_INCLUDES).unwrap();
// Phase 3: Generate REST routes
let rest_config = RestCodegenConfig::new();
let code = generate(&descriptor_bytes, &rest_config).unwrap();
std::fs::write(format!("{out_dir}/rest_routes.rs"), code).unwrap();
}
```
## Configuration
Explicit package mapping (e.g., when using `pub use v1::*;` re-exports):
```rust,ignore
let config = RestCodegenConfig::new()
.package("auth.v1", "auth")
.package("users.v1", "users")
.wrapper_type("crate::core::Uuid")
.extension_type("my_app::AuthInfo")
.public_methods(&["Login", "SignUp"])
.sse_keep_alive_secs(30);
```
### `RestCodegenConfig` Options
| `.package(proto, rust)` | auto-discover | Proto package → Rust module mapping |
| `.extension_type(path)` | `None` | Extension type for Axum `Extension<T>` extraction |
| `.public_methods(list)` | empty | Methods whose paths skip auth middleware |
| `.wrapper_type(path)` | `None` | Rust type for single-field wrapper messages (UUID) |
| `.proto_root(path)` | `"crate"` | Root module for proto types |
| `.runtime_crate(path)` | `"tonic_rest"` | Path to runtime types |
| `.sse_keep_alive_secs(n)` | `15` | SSE keep-alive interval |
| `.extra_forwarded_headers(&[..])` | empty | Extra HTTP headers to forward to gRPC metadata |
## Feature Flags
| `helpers` | **on** | `dump_file_descriptor_set` and `configure_prost_serde` helpers (adds `prost-build` dep) |
## Serde Attribute Helper
Auto-discover proto fields and apply `#[serde(with)]` attributes:
```rust,ignore
use tonic_rest_build::configure_prost_serde;
configure_prost_serde(
&mut config,
&descriptor_bytes,
PROTO_FILES,
"crate::serde_wkt",
&[(".google.protobuf.Timestamp", "opt_timestamp")],
&[(".my.v1.UserRole", "user_role")],
);
```
## Runtime Dependencies
The generated handler code references types from these crates — ensure they are
in your `[dependencies]`:
| `tonic-rest` | `RestError`, `build_tonic_request`, `sse_error_event` |
| `tonic` | `tonic::Status`, `tonic::Request`, service traits |
| `axum` | Router, extractors, `Json`, `Query`, SSE |
| `futures` | `Stream`, `StreamExt` (streaming endpoints only) |
| `serde_json` | `Json` extractor/response |
## Generated Code
For each service with HTTP annotations:
- `{service}_rest_router(service: Arc<S>) -> Router` — route registration
- Per-method handler functions with proper extractors
- `PUBLIC_REST_PATHS: &[&str]` — paths that bypass authentication middleware
- `all_rest_routes(...)` — combined router for all services
### Handler Variants
| POST/PUT/PATCH | `Json<T>` | `Json<Response>` |
| GET | `Query<T>` | `Json<Response>` |
| DELETE | `T::default()` | `StatusCode::NO_CONTENT` |
| GET (streaming) | `Query<T>` | `Sse<impl Stream>` |
## Planned
- **`additional_bindings`**: Proto `HttpRule.additional_bindings` (multiple REST mappings per
gRPC method) is not supported. Only the primary HTTP binding is processed.
- **Partial body selectors**: Only `body: "*"` (full body) and `body: ""` (no body) are
supported. The `body: "field_name"` partial body binding from the gRPC-HTTP transcoding spec
is not implemented.
- **Repeated WKT fields**: `configure_prost_serde` does not wire serde adapters for
lists of well-known types (e.g. `repeated google.protobuf.Timestamp`). Single fields of these
types work correctly.
For a complete end-to-end example with proto files, `build.rs`, REST handlers, and OpenAPI generation,
see [auth-service-rs](https://github.com/zs-dima/auth-service-rs).
## Companion Crates
| [tonic-rest-core](https://crates.io/crates/tonic-rest-core) | Shared descriptor types | internal |
| [tonic-rest](https://crates.io/crates/tonic-rest) | Runtime types | `[dependencies]` |
| **tonic-rest-build** (this) | Build-time codegen | `[build-dependencies]` |
| [tonic-rest-openapi](https://crates.io/crates/tonic-rest-openapi) | OpenAPI 3.1 generation | CLI / CI |
## Compatibility
| 0.1.x | 0.14 | 0.14 | 0.8 | 1.85 |
## License
[MIT](LICENSE-MIT) OR [Apache-2.0](LICENSE-APACHE)