restful 0.2.0

REST API helpers: validated Axum extractors, response envelopes, pagination, ordering, and field selection
docs.rs failed to build restful-0.2.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Visit the last successful build: restful-0.1.0

restful

Request and response helpers for Rust. Ordering and pagination metadata are always available; the other modules are feature-gated.

Features

Feature What it enables
response (default) JSON API envelopes, metadata, errors, and Axum IntoResponse
axum Validated JSON, form, and query extractors; also enables response
projection Field-selection parsing and validation
macros Macro exports in ordering, pagination, and, when enabled, projection
field-delta Re-exports field_delta::Delta as restful::Delta
pgkit Sort-direction conversion; column selections with projection
utoipa Schema support for runtime types and an enabled field-delta dependency
full All features above

Use default-features = false for ordering and pagination metadata alone. The response feature includes Axum even without the extractor feature. Macro-generated code may need direct Serde, Valid8r, Utoipa, or Pgkit dependencies; enabling a feature does not add imports to your application.

Responses and pagination

Add serde_json = "1" to run this example. It uses the default features:

use restful::pagination::offset::OffsetPaginationMetadata;
use restful::response::{ApiResponse, HttpErrorResponse, ResponseMetadata};

let pagination = OffsetPaginationMetadata::new(40, 1, 20);
let meta = ResponseMetadata::paginated(pagination);
let response = ApiResponse::ok(vec!["example"], Some(meta)).unwrap();
assert_eq!(response.code().as_u16(), 200);
assert_eq!(serde_json::to_value(response).unwrap()["meta"]["pagination"]["nextPage"], 2);

let error = ApiResponse::from(HttpErrorResponse::not_found("Item not found"));
assert_eq!(error.code().as_u16(), 404);

Success envelopes contain data and optional meta; failures contain error with type, message, and optional details. created uses 201, accepted uses 202, and no_content produces an empty 204 response.

Offset metadata clamps page size to at least one and the page to the available range. Cursor metadata sets has_more from whether next_cursor is present.

Add custom metadata with .set(key, serde_json::Value). Custom fields appear directly inside meta, alongside pagination. Start with ResponseMetadata::default() for a response without pagination:

use restful::response::{ApiResponse, ResponseMetadata};
use serde_json::json;

let mut meta = ResponseMetadata::default();
meta.set("requestId", json!("req_123")).unwrap();
meta.set("currency", json!({"display": "AED", "rate": "3.67"})).unwrap();
let response = ApiResponse::ok(vec!["example"], Some(meta)).unwrap();
let json = serde_json::to_value(response).unwrap();
assert_eq!(json["meta"]["requestId"], "req_123");
assert_eq!(json["meta"]["currency"]["display"], "AED");
assert!(json["meta"].get("pagination").is_none());

Repeated custom keys replace their previous values. The "pagination" key is reserved; .set() returns ReservedKeyError for it. Convert typed metadata with serde_json::to_value(value)? before adding it. With utoipa, custom fields are represented by an open object schema rather than individual typed properties.

Validated requests

Enable axum and add direct serde (with derive) and valid8r = "0.2.2" dependencies. This handler accepts only JSON values that pass validation:

use restful::axum::ValidatedJson;
use restful::response::ApiResponse;
use serde::{Deserialize, Serialize};
use valid8r::Validate;

#[derive(Deserialize, Serialize, Validate)]
#[serde(rename_all = "camelCase")]
struct NewItem {
    #[validate(length(min = 1))]
    display_name: String,
}

async fn create(ValidatedJson(item): ValidatedJson<NewItem>) -> Result<ApiResponse, ApiResponse> {
    ApiResponse::created(item).map_err(|_| {
        ApiResponse::internal_server_error("Could not serialize response".into())
    })
}

ValidatedForm validates form data; ValidatedQuery validates URL queries. They all require DeserializeOwned + valid8r::Validate.

Validation failures use 422. JSON data errors with a field path also use 422; root JSON data errors, malformed input, and form/query deserialization errors use 400. Body limits use 413; rejected body content types use 415.

Validation error keys follow a camelCase policy. Use #[serde(rename_all = "camelCase")] and avoid conflicting field renames when keys must match the request. Nested list errors retain their indices as object keys, such as "items": {"1": {...}}.

Built-in deserialization messages are sanitized, but error paths can include client-sent keys. Keep input out of custom deserializer messages. Validation errors retain Valid8r parameters; use #[validate(sensitive)] on sensitive fields and keep secrets out of custom messages and parameters.

Field selection and PATCH

With projection, Projection<T>::from(String) stores parsing errors rather than returning a Result. Check is_valid() or call validate() before using its fields. Deserializing alone does not run validation.

With projection and macros, the ProjectionField and PartialResponse derives support parsing field names and building partial response types. They do not apply a projection to an existing response. See Projection's API documentation for selection syntax and ProjectionField's for Pgkit column mapping.

End to end, the application checks the parsed fields and fills the partial:

use restful::projection::{PartialResponse, Projection, ProjectionField};

#[derive(serde::Serialize, ProjectionField, PartialResponse)]
#[projection_field(skip(utoipa))]
struct Item {
    id: u32,
    note: Option<String>,
}

// From ?fields=id,note
let fields = Projection::<ItemField>::from("id,note".to_owned());
assert!(fields.is_valid());

let mut partial = PartialItem::default();
if fields.fields().contains(&ItemField::Id) {
    partial.id = Some(7);
}
if fields.fields().contains(&ItemField::Note) {
    partial.note = Some(None); // selected, but null
}
assert_eq!(
    serde_json::to_value(partial).unwrap(),
    serde_json::json!({ "id": 7, "note": null })
);

With field-delta, restful::Delta<T> represents unchanged, clear, and set PATCH values. Use #[serde(default)] on a Delta field to treat omitted input as unchanged.