9 releases
0.16.3 | Jan 4, 2025 |
---|---|
0.16.2 | Jan 2, 2025 |
0.15.7 | Jan 1, 2025 |
0.15.3 | Dec 31, 2024 |
0.2.0 | Dec 31, 2024 |
#33 in #error-response
1,174 downloads per month
Used in 6 crates
(via api-response)
25KB
159 lines
API Response Library
This library provides a consistent structure for API responses, including success and error handling.
Features
- Structured and unified API response format.
- Includes meta for both success and error responses.
- Support segmented error status codes.
- Supports flexible serialization formats like JSON and Protobuf.
- Integration with the Salvo framework for HTTP handling (see examples).
Usage
Run the following Cargo command in your project directory:
cargo add api-response
Unified API Response Structure
1. Well-defined Structure
Top-Level Fields
Field Name | Type & Example | Required | Meaning | Description |
---|---|---|---|---|
status | "success" or "error" |
Yes | Status of the request | Indicates whether the request was successful. |
data | Any |
Required in success responses | Response data | The data returned when the request is successful. |
error | ApiError object |
Required in error responses | Error information | The error information object returned when the request fails. |
meta | DefaultMeta object |
No | Metadata information | Metadata about the request. |
ApiError
Object Fields
Field Name | Type & Example | Required | Meaning | Description |
---|---|---|---|---|
code | 404 unsigned 32-bit integer |
Yes | Error code | A code that identifies the type of error. |
message | "error message" |
Yes | Error message | Text description of the error. |
details | { "key": "value" } |
No | Error details | The field is of the map<string, string> type and can be used to pass the front-end display configuration, error details and so on. |
DefaultMeta
Object Fields
Meta Field | Type & Example | Required | Meaning | Description |
---|---|---|---|---|
requestId |
"abc4567890" |
No | Request tracking information | Included in the Response Body to maintain consistent data structure in non-standard protocols such as RPC, MQ, etc. |
user |
{ "id": "user-123", "roles": ["admin", "editor"] } |
No | The permission information of the current user, etc. | Notifies the client about the permissions the current user has for this request. |
pagination |
{ "currentPage": 1, "pageSize": 10, "totalPages": 5, "totalRecords": 50, "nextPage": 2, "prevPage": null } |
No | Pagination information | Helps clients with pagination navigation, displaying, and retrieving more data. |
rateLimit |
{ "limit": 1000, "remaining": 990, "restoreRate": 50, "resetAt": "2021-01-01T00:00:00Z" } |
No | Rate limiting information | Includes the limit, remaining calls, restore rate, and reset time, helping clients manage API call frequencies to avoid rate limit issues. |
cost |
{ "actualCost": 10, "requestedQueryCost": 10, "executionTime": "250ms" } |
No | Cost statistics | Provides the cost statistics of the request operation, helping clients understand API resource consumption. |
apiVersion |
"v1.0.1" |
No | Current API version information | Ensures the API version consistency between client and server, beneficial for compatibility management, suitable for internal use or frequently iterated APIs. |
Well-defined JSON Examples
Success Response Example:
{
"status": "success",
"data": "success data",
"meta": {
"requestId": "abc4567890",
"user": {
"id": "user-123",
"roles": ["admin", "editor"]
},
"pagination": {
"currentPage": 1,
"pageSize": 10,
"totalPages": 5,
"totalRecords": 50,
"nextPage": 2,
"prevPage": null
},
"rateLimit": {
"limit": 1000,
"remaining": 990,
"restoreRate": 50,
"resetAt": "2021-01-01T00:00:00Z"
},
"cost": {
"actualCost": 10,
"requestedQueryCost": 10,
"executionTime": "250ms"
},
"apiVersion": "v1.0.1"
}
}
Error Response Example:
{
"status": "error",
"error": {
"code": 404,
"message": "error message",
"details": {
"key": "value"
}
},
"meta": {
"requestId": "abc4567890",
"user": {
"id": "user-123",
"roles": ["admin", "editor"]
},
"pagination": {
"currentPage": 1,
"pageSize": 10,
"totalPages": 5,
"totalRecords": 50,
"nextPage": 2,
"prevPage": null
},
"rateLimit": {
"limit": 1000,
"remaining": 990,
"restoreRate": 50,
"resetAt": "2021-01-01T00:00:00Z"
},
"cost": {
"actualCost": 10,
"requestedQueryCost": 10,
"executionTime": "250ms"
},
"apiVersion": "v1.0.1"
}
}
2. Lightly-defined Structure
Enable the lite
feature to use the lightly-defined structure.
api-response = { version = ">=0.13.0", features = ["lite"] }
The difference between the lightly-defined structure and the well-defined structure is that the status
field is replaced by the error.code
field, and when the "code" equals 0, it indicates a successful response.
Lightly-defined JSON Examples
Success Response Example:
{
"code": 0,
"data": "success data",
"meta": {
"requestId": "abc4567890",
"user": {
"id": "user-123",
"roles": ["admin", "editor"]
},
"pagination": {
"currentPage": 1,
"pageSize": 10,
"totalPages": 5,
"totalRecords": 50,
"nextPage": 2,
"prevPage": null
},
"rateLimit": {
"limit": 1000,
"remaining": 990,
"restoreRate": 50,
"resetAt": "2021-01-01T00:00:00Z"
},
"cost": {
"actualCost": 10,
"requestedQueryCost": 10,
"executionTime": "250ms"
},
"apiVersion": "v1.0.1"
}
}
Error Response Example:
{
"code": 404,
"error": {
"message": "error message",
"details": {
"key": "value"
}
},
"meta": {
"requestId": "abc4567890",
"user": {
"id": "user-123",
"roles": ["admin", "editor"]
},
"pagination": {
"currentPage": 1,
"pageSize": 10,
"totalPages": 5,
"totalRecords": 50,
"nextPage": 2,
"prevPage": null
},
"rateLimit": {
"limit": 1000,
"remaining": 990,
"restoreRate": 50,
"resetAt": "2021-01-01T00:00:00Z"
},
"cost": {
"actualCost": 10,
"requestedQueryCost": 10,
"executionTime": "250ms"
},
"apiVersion": "v1.0.1"
}
}
Error Code Specification
The error_code
module provides the ability to construct standardized error-code information.
The standardized error code is segmented and divided according to the decimal literals of unsigned 32-bit integer
.
-
The format is:
{ErrType: 1000-4293} | {ErrPath-Root: 0-99} | {ErrPath-Parent: 0-99} | {ErrPath: 0-99}
So, The value range of the error code is from
1000000000 to 4293999999
inclusive. -
A proposed specification for grouping types of error codes:
Error Type Range Error Type Category Description 1000-1999
Client-Side Error Handles all issues related to user interface interactions, including Web
,Mobile
, andDesktop
clients.2000-2999
Business Service Error Covers issues related to the operation of business layer services. 3000-3999
Infrastructure Service Error Includes database operations, middleware, system observability, network communication, gateway and proxy issues, and other infrastructure service-related errors. 4000-4293
Uncategorized Error Other errors that cannot be classified into specific categories. Suggestion: Except for grouping by the first byte, there is no need to distinguish second-level and third-level parts for error code types (they can just be increased according to the serial numbers), because this is highly likely to lead to the exhaustion of number resources.
-
It is recommended to use the division method of "
product(ErrPath-Root)
|system(ErrPath-Parent)
|module(ErrPath)
" for the error path.
Example
Example of data construction.
use api_response::prelude::*;
#[test]
fn success_json() {
const SUCCESS: &str = if cfg!(feature = "lite") {
r##"{"code":0,"data":"success data","meta":{"requestId":"request_id","pagination":{"currentPage":1,"pageSize":0,"totalPages":0,"totalRecords":0,"nextPage":null,"prevPage":null},"custom":{"key":"value"}}}"##
} else {
r##"{"status":"success","data":"success data","meta":{"requestId":"request_id","pagination":{"currentPage":1,"pageSize":0,"totalPages":0,"totalRecords":0,"nextPage":null,"prevPage":null},"custom":{"key":"value"}}}"##
};
let mut api_response = ApiResponse::new_success(
"success data",
DefaultMeta::new()
.with_request_id("request_id")
.with_pagination(Some(Pagination::default().with_current_page(1)))
.insert_custom("key", "value"),
);
println!("{}", serde_json::to_string_pretty(&api_response).unwrap());
let s = serde_json::to_string(&api_response).unwrap();
assert_eq!(SUCCESS, s);
api_response = serde_json::from_str(SUCCESS).unwrap();
let e = serde_json::to_string(&api_response).unwrap();
assert_eq!(SUCCESS, e);
}
#[test]
fn error_json() {
const ERROR: &str = if cfg!(feature = "lite") {
r##"{"code":404,"error":{"message":"error message","details":{"key":"value","source":"invalid digit found in string"}},"meta":{"requestId":"request_id","pagination":{"currentPage":1,"pageSize":0,"totalPages":0,"totalRecords":0,"nextPage":null,"prevPage":null},"custom":{"key":"value"}}}"##
} else {
r##"{"status":"error","error":{"code":404,"message":"error message","details":{"key":"value","source":"invalid digit found in string"}},"meta":{"requestId":"request_id","pagination":{"currentPage":1,"pageSize":0,"totalPages":0,"totalRecords":0,"nextPage":null,"prevPage":null},"custom":{"key":"value"}}}"##
};
let mut api_response = ApiResponse::<(), _>::new_error(
ApiError::new(404u32, "error message")
.with_detail("key", "value")
.with_source("@".parse::<u8>().unwrap_err(), true),
DefaultMeta::new()
.with_request_id("request_id")
.with_pagination(Some(Pagination::default().with_current_page(1)))
.insert_custom("key", "value"),
);
println!("{}", serde_json::to_string_pretty(&api_response).unwrap());
let e = serde_json::to_string(&api_response).unwrap();
assert_eq!(ERROR, e);
api_response = serde_json::from_str(ERROR).unwrap();
let e = serde_json::to_string(&api_response).unwrap();
assert_eq!(ERROR, e);
}
Example of server
use std::num::ParseIntError;
use api_response::{error_code::*, prelude::*};
use salvo::prelude::*;
use serde_json::{Value, json};
/// get user
#[cfg_attr(feature = "salvo", endpoint)]
#[cfg_attr(not(feature = "salvo"), handler)]
async fn get_user() -> Json<ApiResponse<Value, DefaultMeta>> {
let user = json!({
"id": 123,
"name": "Andeya Lee",
"email": "andeya.lee@example.com"
});
Json(user.api_response_with_meta(DefaultMeta::new().with_request_id("abc-123")))
}
const EP_LV1: ErrPathRoot = X00("product");
const EP_LV2: ErrPathParent = EP_LV1.Y01("system");
const EP_LV3: ErrPath = EP_LV2.Z20("module");
/// get error
#[cfg_attr(feature = "salvo", endpoint)]
#[cfg_attr(not(feature = "salvo"), handler)]
async fn get_error() -> Json<ApiResponse<Value, ()>> {
let err: ParseIntError = "@".parse::<u8>().unwrap_err();
let details = [("email".to_string(), "Invalid email format".to_string())]
.iter()
.cloned()
.collect();
let error = api_err!(ety_grpc::INVALID_ARGUMENT, EP_LV3)
.with_details(details)
.with_source(err, true);
println!("error={:?}", error.downcast_ref::<ParseIntError>().unwrap());
Json(Err(error).into())
}
#[tokio::main]
async fn main() {
#[allow(unused_mut)]
let mut router = Router::new()
.get(get_user)
.push(Router::with_path("error").get(get_error));
#[cfg(feature = "salvo")]
{
let doc = OpenApi::new("API-Response", "1").merge_router(&router);
router = router
.push(doc.into_router("/api-doc/openapi.json"))
.push(SwaggerUi::new("/api-doc/openapi.json").into_router("swagger-ui"));
}
Server::new(TcpListener::new("127.0.0.1:7878").bind().await)
.serve(router)
.await;
}
- Get User: http://localhost:7878/
- Get Error: http://localhost:7878/error
- If the
salvo
feature is enabled, api-doc can be accessed: http://localhost:7878/swagger-ui
Dependencies
~210–650KB
~15K SLoC