教你更優雅地寫 API 之規範響應資料

Jiannei發表於2020-12-19

圖片

前言

在推出 lumen-api-starter 以後,收到了不少的關注和反饋,先在此感謝各位朋友萌 ?

關於 lumen-api-starter 的介紹可以參考上一篇是時候使用 Lumen 8 + API Resource 開發專案了!。本篇是在前一篇的基礎上,重新整理並獨立出了 package,可以同時支援最新版本 Laravel 和 Lumen 專案。

Package 地址:laravel-response
Laravel 版本 Api 開發初始化專案:laravel-api-starter
Lumen 版本 Api 開發初始化專案:lumen-api-starter

回到正題, 在用 Laravel 或 Lumen 寫 API 專案前,通常需要先定義一些專案規範,來讓後續的開發體驗更舒適,包含有:

  • 規範統一響應資料結構:成功操作、失敗操作以及異常操作響應
  • 使用列舉來管理專案中的常量,減少 bug,提高擴充套件性
  • 更有效地記錄日誌,來提高線上排查問題效率
  • 其他。..(規劃中)

實現過程

RESTful 服務最佳實踐 :如何去設計 Http 狀態碼以及資料返回格式。

思路

  • 儘可能地遵循 Laravel 思維進行擴充套件,符合一定規範
  • 儘量少的依賴安裝,最好是 0 依賴,不額外增加負擔
  • 儘量完善的單元測試,保證程式碼質量(關於使用示例可以跳過下面的介紹直接檢視 github.com/Jiannei/laravel-respons... 測試用例)
  • 實現需要簡潔,使用需要優雅

功能

  • 統一的資料響應格式,固定包含:codestatusdatamessageerror
  • 內建 Http 標準狀態碼支援,同時支援擴充套件 ResponseCodeEnum 來根據不同業務模組定義響應碼
  • 響應碼 coed 對應描述資訊 message 支援本地化,支援配置多語言
  • 合理地返回 Http 狀態碼
  • 根據 debug 開關,合理返回異常資訊、驗證異常資訊等
  • 支援格式化 Laravel 的 Api ResourceApi Resource CollectionPaginator(簡單分頁)、LengthAwarePaginator(普通分頁)、Eloquent\ModelEloquent\Collection,以及簡單的 arraystring等格式資料返回
  • 分頁資料格式化後的結果與使用 league/fractal (DingoApi 使用該擴充套件進行資料轉換)的 transformer 轉換後的格式保持一致,也就是說,可以順滑地從 Laravel Api Resource 切換到 league/fractal

規範

  • 合適的 Http 狀態碼,可以讓客戶端 / 瀏覽器更好地理解 Http 響應

教你更優雅地寫 API 之規範響應資料

  • 格式固定
{
    "status": "success",// 描述 HTTP 響應結果:HTTP 狀態響應碼在 500-599 之間為”fail”,在 400-499 之間為”error”,其它均為”success”
    "code": 200,// 包含一個整數型別的 HTTP 響應狀態碼,也可以是業務描述操作碼,比如 200001 表示註冊成功
    "message": "操作成功",// 多語言的響應描述
    "data": {// 實際的響應資料
        "nickname": "Joaquin Ondricka",
        "email": "lowe.chaim@example.org"
    },
    "error": {}// 異常時的除錯資訊
}

需求

在不使用任何 package 的情況下,在 Laravel 中響應 API Json 格式資料,通常是下面這個樣子:

return response()->json($data, $status, $headers, $options);

但實際開發場景,會有很多種資料返回需求:

  • 更多時候只是簡單的成功和失敗響應,所以需要有快捷的 successfail 格式化方法
  • 成功響應可能包含有:User::all()User::first()UserResourceUserCollectionUser::paginate()User::simplePaginate()Collection和普通的 Array等,希望這些不同型別的資料都能格式化成統一的結構
  • 失敗的響應,通常就是根據不同的業務場景,返回不同的錯誤碼和錯誤描述
  • 異常響應,對於表單驗證、Http 等異常情況,能夠針對是否開啟 debug 有不同響應,並且格式與前面統一

定義業務操作碼

namespace App\Repositories\Enums;

use Jiannei\Response\Laravel\Repositories\Enums\ResponseCodeEnum as BaseResponseCodeEnum;

class ResponseCodeEnum extends BaseResponseCodeEnum
{
    // 業務操作正確碼:1xx、2xx、3xx 開頭,後拼接 3 位
    // 200 + 001 => 200001,也就是有 001 ~ 999 個編號可以用來表示業務成功的情況,當然你可以根據實際需求繼續增加位數,但必須要求是 200 開頭
    // 舉個例子:你可以定義 001 ~ 099 表示系統狀態;100 ~ 199 表示授權業務;200 ~ 299 表示使用者業務。..
    const SERVICE_REGISTER_SUCCESS = 200101;
    const SERVICE_LOGIN_SUCCESS = 200102;

    // 客戶端錯誤碼:400 ~ 499 開頭,後拼接 3 位
    const CLIENT_PARAMETER_ERROR = 400001;
    const CLIENT_CREATED_ERROR = 400002;
    const CLIENT_DELETED_ERROR = 400003;

    const CLIENT_VALIDATION_ERROR = 422001; // 表單驗證錯誤

    // 服務端操作錯誤碼:500 ~ 599 開頭,後拼接 3 位
    const SYSTEM_ERROR = 500001;
    const SYSTEM_UNAVAILABLE = 500002;
    const SYSTEM_CACHE_CONFIG_ERROR = 500003;
    const SYSTEM_CACHE_MISSED_ERROR = 500004;
    const SYSTEM_CONFIG_ERROR = 500005;

    // 業務操作錯誤碼(外部服務或內部服務呼叫。..)
    const SERVICE_REGISTER_ERROR = 500101;
    const SERVICE_LOGIN_ERROR = 500102;
}

本地化操作碼描述

// resources/lang/zh-CN/enums.php
use App\Repositories\Enums\ResponseCodeEnum;

return [
    // 響應狀態碼
    ResponseCodeEnum::class => [
        // 成功
        ResponseCodeEnum::HTTP_OK => '操作成功', // 自定義 HTTP 狀態碼返回訊息
        ResponseCodeEnum::HTTP_INTERNAL_SERVER_ERROR => '操作失敗', // 自定義 HTTP 狀態碼返回訊息
        ResponseCodeEnum::HTTP_UNAUTHORIZED => '授權失敗',

        // 業務操作成功
        ResponseCodeEnum::SERVICE_REGISTER_SUCCESS => '註冊成功',
        ResponseCodeEnum::SERVICE_LOGIN_SUCCESS => '登入成功',

        // 客戶端錯誤
        ResponseCodeEnum::CLIENT_PARAMETER_ERROR => '引數錯誤',
        ResponseCodeEnum::CLIENT_CREATED_ERROR => '資料已存在',
        ResponseCodeEnum::CLIENT_DELETED_ERROR => '資料不存在',
        ResponseCodeEnum::CLIENT_VALIDATION_ERROR => '表單驗證錯誤',

        // 服務端錯誤
        ResponseCodeEnum::SYSTEM_ERROR => '伺服器錯誤',
        ResponseCodeEnum::SYSTEM_UNAVAILABLE => '伺服器正在維護,暫不可用',
        ResponseCodeEnum::SYSTEM_CACHE_CONFIG_ERROR => '快取配置錯誤',
        ResponseCodeEnum::SYSTEM_CACHE_MISSED_ERROR => '快取未命中',
        ResponseCodeEnum::SYSTEM_CONFIG_ERROR => '系統配置錯誤',

        // 業務操作失敗:授權業務
        ResponseCodeEnum::SERVICE_REGISTER_ERROR => '註冊失敗',
        ResponseCodeEnum::SERVICE_LOGIN_ERROR => '登入失敗',
    ],
];

使用示例

成功響應

  • 示例程式碼
public function index()
{
    $users = User::all();

    return Response::success(new UserCollection($users));
}

public function paginate()
{
    $users = User::paginate(5);

    return Response::success(new UserCollection($users));
}

public function simplePaginate()
{
    $users = User::simplePaginate(5);

    return Response::success(new UserCollection($users));
}

public function item()
{
    $user = User::first();

    return Response::success(new UserResource($user));
}

public function array()
{
    return Response::success([
        'name' => 'Jiannel',
        'email' => 'longjian.huang@foxmail.com'
    ],'', ResponseCodeEnum::SERVICE_REGISTER_SUCCESS);
}
  • 返回全部資料
{
    "status": "success",
    "code": 200,
    "message": "操作成功",
    "data": [
        {
            "nickname": "Joaquin Ondricka",
            "email": "lowe.chaim@example.org"
        },
        {
            "nickname": "Jermain D'Amore",
            "email": "reanna.marks@example.com"
        },
        {
            "nickname": "Erich Moore",
            "email": "ernestine.koch@example.org"
        }
    ],
    "error": {}
}
  • 分頁資料
{
    "status": "success",
    "code": 200,
    "message": "操作成功",
    "data": {
        "data": [
            {
                "nickname": "Joaquin Ondricka",
                "email": "lowe.chaim@example.org"
            },
            {
                "nickname": "Jermain D'Amore",
                "email": "reanna.marks@example.com"
            },
            {
                "nickname": "Erich Moore",
                "email": "ernestine.koch@example.org"
            },
            {
                "nickname": "Eva Quitzon",
                "email": "rgottlieb@example.net"
            },
            {
                "nickname": "Miss Gail Mitchell",
                "email": "kassandra.lueilwitz@example.net"
            }
        ],
        "meta": {
            "pagination": {
                "count": 5,
                "per_page": 5,
                "current_page": 1,
                "total": 12,
                "total_pages": 3,
                "links": {
                    "previous": null,
                    "next": "http://laravel-api.test/api/users/paginate?page=2"
                }
            }
        }
    },
    "error": {}
}
  • 返回簡單分頁資料
{
    "status": "success",
    "code": 200,
    "message": "操作成功",
    "data": {
        "data": [
            {
                "nickname": "Joaquin Ondricka",
                "email": "lowe.chaim@example.org"
            },
            {
                "nickname": "Jermain D'Amore",
                "email": "reanna.marks@example.com"
            },
            {
                "nickname": "Erich Moore",
                "email": "ernestine.koch@example.org"
            },
            {
                "nickname": "Eva Quitzon",
                "email": "rgottlieb@example.net"
            },
            {
                "nickname": "Miss Gail Mitchell",
                "email": "kassandra.lueilwitz@example.net"
            }
        ],
        "meta": {
            "pagination": {
                "count": 5,
                "per_page": 5,
                "current_page": 1,
                "links": {
                    "previous": null,
                    "next": "http://laravel-api.test/api/users/simple-paginate?page=2"
                }
            }
        }
    },
    "error": {}
}
  • 返回單條資料
{
    "status": "success",
    "code": 200,
    "message": "操作成功",
    "data": {
        "nickname": "Joaquin Ondricka",
        "email": "lowe.chaim@example.org"
    },
    "error": {}
}

其他快捷方法

Response::accepted();
Response::created();
Response::noContent();

失敗響應

不指定 meesage

public function fail()
{
    Response::fail();// 不需要加 return
}
  • 未配置多語言響應描述,返回資料
{
    "status": "fail",
    "code": 500,
    "message": "Http internal server error",
    "data": {},
    "error": {}
}
  • 配置多語言描述後,返回資料
{
    "status": "fail",
    "code": 500,
    "message": "操作失敗",
    "data": {},
    "error": {}
}

指定 message

public function fail()
{
    Response::fail('error');// 不需要加 return
}

返回資料

{
    "status": "fail",
    "code": 500,
    "message": "error",
    "data": {},
    "error": {}
}

指定 code

public function fail()
{
    Response::fail('',ResponseCodeEnum::SERVICE_LOGIN_ERROR);
}

返回資料

{
    "status": "fail",
    "code": 500102,
    "message": "登入失敗",
    "data": {},
    "error": {}
}

其他快捷方法

Response::errorBadRequest();
Response::errorUnauthorized();
Response::errorForbidden();
Response::errorNotFound();
Response::errorMethodNotAllowed();
Response::errorInternal();

異常響應

對於異常的資料格式化,需額外在 app/Exceptions/Handler.php 中 引入 use Jiannei\Response\Laravel\Support\Traits\ExceptionTrait; 引入以後,對於 ajax 請求產生的異常都會進行格式化資料返回。

(Lumen 中為達到同樣效果,還需在 app/Http/Controllers/Controller.php 中引入 ExceptionTrait

  • 表單驗證異常
{
    "status": "error",
    "code": 422,
    "message": "驗證失敗",
    "data": {},
    "error": {
        "email": [
            "The email field is required."
        ]
    }
}
  • Controller 以外丟擲異常返回

可以直接使用 abort 輔助函式直接丟擲 HttpException 異常

abort(ResponseCodeEnum::SERVICE_LOGIN_ERROR);

// 返回資料

{
    "status": "fail",
    "code": 500102,
    "message": "登入失敗",
    "data": {},
    "error": {}
}
  • 其他異常

開啟 debug

{
    "status": "error",
    "code": 404,
    "message": "Http not found",
    "data": {},
    "error": {
        "message": "",
        "exception": "Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException",
        "file": "/home/vagrant/code/laravel-api-starter/vendor/laravel/framework/src/Illuminate/Routing/AbstractRouteCollection.php",
        "line": 43,
        "trace": [
            {
                "file": "/home/vagrant/code/laravel-api-starter/vendor/laravel/framework/src/Illuminate/Routing/RouteCollection.php",
                "line": 162,
                "function": "handleMatchedRoute",
                "class": "Illuminate\\Routing\\AbstractRouteCollection",
                "type": "->"
            },
            {
                "file": "/home/vagrant/code/laravel-api-starter/vendor/laravel/framework/src/Illuminate/Routing/Router.php",
                "line": 646,
                "function": "match",
                "class": "Illuminate\\Routing\\RouteCollection",
                "type": "->"
            },
            ...
        ]
    }
}

關閉 debug

{
    "status": "error",
    "code": 404,
    "message": "Http not found",
    "data": {},
    "error": {}
}

One more thing?

回顧一下,這些封裝全部都是基於 response()->json() ,即返回的是 JsonResponse 物件,所以我們依舊可以繼續鏈式呼叫該物件上的方法。

// 設定 HTTP 響應碼
return Response::success(new UserResource($user))->setStatusCode(ResponseCodeEnum::HTTP_CREATED);

其他

依照慣例,如果對您的日常工作有所幫助或啟發,歡迎三連 star + fork + follow

如果有任何批評建議,通過郵箱(longjian.huang@foxmial.com)的方式可以聯絡到我。

總之,歡迎各路英雄好漢。

QQ 群:1105120693

本作品採用《CC 協議》,轉載必須註明作者和本文連結
Peace & Love ❤️

相關文章