Crates.io | api-response |
lib.rs | api-response |
version | 0.16.3 |
created_at | 2024-10-26 17:09:08.957055+00 |
updated_at | 2025-01-04 02:35:58.95005+00 |
description | A consistent structure for API responses, including success and error handling. |
homepage | |
repository | https://github.com/andeya/api-response |
max_upload_size | |
id | 1424000 |
size | 188,803 |
This library provides a consistent structure for API responses, including success and error handling.
Run the following Cargo command in your project directory:
cargo add api-response
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 FieldsField 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 FieldsMeta 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. |
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"
}
}
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.
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"
}
}
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 , and Desktop 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.
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);
}
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;
}
salvo
feature is enabled, api-doc can be accessed: http://localhost:7878/swagger-ui