#response #http #api #error-response #rpc

api-response

A consistent structure for API responses, including success and error handling

8 releases

new 0.7.1 Oct 29, 2024
0.7.0 Oct 29, 2024
0.6.5 Oct 28, 2024

#1543 in Network programming

Download history 392/week @ 2024-10-22

395 downloads per month
Used in cubix

MIT license

30KB
618 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.
  • 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

Or add the following line to your Cargo.toml:

api-response = { version = "0.7", features = ["try"] }

Format

  • Success example:

    {
        "status": "success",
        "data": "success data",
        "meta": {
            "requestId": "request_id",
            "links": {
                "selfLink": "http:://andeya.example.com/b",
                "next": "http:://andeya.example.com/c",
                "prev": "http:://andeya.example.com/a"
            },
            "custom": {
                "key": "value"
            }
        }
    }
    
  • Error example

    {
        "status": "error",
        "error": {
            "code": 404,
            "message": "error message",
            "details": {
                "key": "value"
            }
        },
        "meta": {
            "requestId": "request_id",
            "links": {
                "selfLink": "http:://andeya.example.com/b",
                "next": "http:://andeya.example.com/c",
                "prev": "http:://andeya.example.com/a"
            },
            "custom": {
                "key": "value"
            }
        }
    }
    

Example

Example of data construction.

use api_response::*;

#[test]
fn success_json() {
    const SUCCESS: &str = r##"{"status":"success","data":"success data","meta":{"requestId":"request_id","links":{"selfLink":"http:://andeya.example.com/b","next":"http:://andeya.example.com/c","prev":"http:://andeya.example.com/a"},"custom":{"key":"value"}}}"##;
    let api_response = ApiResponse::from_success(
        "success data",
        DefaultMeta::new("request_id")
            .with_links_info(
                "http:://andeya.example.com/b",
                Some("http:://andeya.example.com/c"),
                Some("http:://andeya.example.com/a"),
            )
            .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);
}

#[test]
fn error_json() {
    const ERROR: &str = r##"{"status":"error","error":{"code":404,"message":"error message","details":{"key":"value"}},"meta":{"requestId":"request_id","links":{"selfLink":"http:://andeya.example.com/b","next":"http:://andeya.example.com/c","prev":"http:://andeya.example.com/a"},"custom":{"key":"value"}}}"##;
    let api_response = ApiResponse::<(), _>::from_error(
        ApiError::new(404, "error message")
            .with_detail("key", "value")
            .with_source("@".parse::<u8>().unwrap_err()),
        DefaultMeta::new("request_id")
            .with_links_info(
                "http:://andeya.example.com/b",
                Some("http:://andeya.example.com/c"),
                Some("http:://andeya.example.com/a"),
            )
            .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);
}

Example of server

use std::num::ParseIntError;

use api_response::{ApiResponse, DefaultMeta, ApiError};
use salvo::prelude::*;
use serde_json::{json, Value};

/// 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, DefaultMeta::new("abc-123")).into())
}

/// 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 = ApiError::new(400, "Invalid input data").with_details(details).with_source(err);
    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

~0.4–16MB
~242K SLoC