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

Download history 1145/week @ 2024-12-31 29/week @ 2025-01-07

1,174 downloads per month
Used in 6 crates (via api-response)

MIT license

25KB
159 lines

API Response Library

This library provides a consistent structure for API responses, including success and error handling.

GitHub last commit Crates.io Docs

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, 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.

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;
}

Dependencies

~210–650KB
~15K SLoC