如何用 OpenAPI 在 Express 中構建更好的 API

我將在這篇文章中分享在 Express 中構建強大的 REST API 的方法。首先,我會介紹構建 REST API 的一些挑戰,然後提出一個使用開放標準的解決方案。

本文並非一篇關於 Node.js、Express.js 或 REST API 的介紹。如果你需要複習,請在深入研究本文內容之前查看這些鏈接。

我喜歡 Node.js 那極具靈活性和易用性的生態。這個社區充滿活力,並且你可以用你已經掌握的語言在幾分鐘內設置一個 REST API。

在應用的前後端使用相同的編程語言是一件很有價值的事。這使我們在瀏覽代碼庫時可以減少上下文切換,從而變得更輕鬆。全棧開發者可以快速切換技術棧,共享代碼也變得輕而易舉。

儘管如此,隨着 MVP 成長爲成熟的生產環境應用程序和開發團隊規模的擴大,這種靈活性也帶來了挑戰。

使用 REST API 的挑戰

無論你使用哪種技術棧,當代碼庫和團隊規模增長時,都會面臨許多挑戰。

在本文中,我將描述通過 REST API 暴露業務邏輯的 Express.js 應用程序所帶來的挑戰,以小見大。

無論 API 消費者的性質如何(網頁、移動應用、第三方後端),隨着他們的成長,他們都可能面臨以下一個(或多個)挑戰:

1. 更難做出改變

在文檔不夠明確時,在 REST API 的任何一方進行修改都變得更加困難。

舉個例子,假設你有一個 REST 端點,可以返回一個特定的用戶的名字。在即將新增的功能中,你可能需要修改這個 API 使其返回年齡。這可能會潛在地破壞網絡應用和移動應用。

你可以設置集成測試來一定程度上避免這個問題,但你仍然會嚴重依賴開發人員來手動覆蓋所有的邊界情況。這需要大量的時間和精力,而且你永遠無法 100% 確定這些變化不會破壞應用程序。

2. 缺少(及時更新的)文檔

文檔是構建 REST API 時的另一個敏感話題。我堅信在大多數情況下,代碼本身應該足以代替一部分文檔。

也就是說,REST API 在開發中會變得越來越複雜,檢查代碼中每個端點的安全性、參數和可能的響應也隨之變得繁瑣且耗時。這就減慢了開發的速度,也給 bug 進入系統留下了隱患。

即使團隊致力於在一個獨立於代碼的文檔中手動保持文檔的更新,也很難 100% 確保它反映了代碼的情況。

3. 公共 API

這並不適用於所有的應用程序,但在某些情況下,一個應用程序可能需要向第三方暴露一系列的功能。對於這種情況,第三方有可能會在我們暴露的 API 之上構建核心功能。

這意味着我們不能以更新我們的私有 API 的同樣速度來修改這些公共 API。一旦修改了公共 API,第三方應用程序可能會因此崩潰,而這正是我們應該不惜一切代價避免的事情。

公共 API 所暴露的內容應該是明確的,並且可以簡單地進行開發,以限制內部和外部開發團隊之間所需的來回溝通的數量。

4. 手動集成測試

當應用程序的開發沒有與之匹配的周密計劃時,很有可能 API 所提供的內容和 API 消費者期望的內容被深埋在代碼中。

對於僅有少量的內部端點的系統來說,這並不是一個大問題。但隨着 API 接口數量的增長,修改現有的端點需要在整個系統中遵循麪包屑,以確保消費者期望得到的東西與提供的東西是相等的。

這個問題可以通過對系統的不同部分之間進行集成測試來緩解。但是人工完成這件事的工作量非常巨大的,並且如果沒做好的話,可能會在系統實際上不能正常工作的時候讓開發人員誤以爲系統狀態良好。

提出的解決方案

我們已經看到了構建 REST API 所帶來的固有挑戰。在下一節中,我們將使用開放標準構建一個示例 Express 項目,以解決這些挑戰。

API 標準規範

前面部分描述的挑戰已經存在很長時間了,所以面對這個問題,我們最好查看現有的解決方案,而不是重新發明輪子。

許多標準嘗試對 REST API 進行規範化定義(RAML、JsonAPI、OpenAPI......)。這些項目的共同目標是使開發人員更容易定義他們的 API 行爲,以便跨多種語言的服務器和客戶端能夠 “共說一種語言”。

有了某種形式的 API 規範,可以解決許多挑戰,因爲在許多情況下,可以從這些規範自動生成客戶端 SDK、測試、模擬服務器和文檔。

一種我最喜歡的規範是 OpenAPI(原名 Swagger)。它有一個很大的社區,並且有很多用於 Express 的工具。這可能不是所有 REST API 項目中的最佳工具,因此請在爲你自己的項目選擇規範之前進行額外的研究,以確保該規範的工具和支持對你的項目有幫助。

示例的背景

在這個示例中,假設我們正在構建一個待辦事項列表管理應用。用戶可以通過訪問一個 web 應用來獲取、創建、編輯和刪除待辦事項,這些待辦事項被保存在後端。

在這個例子中,後端使用一個 Express.js 應用程序,它將通過 REST API 暴露以下功能:

對於一個真實的待辦事項管理應用來說,上面的功能有點過度簡化,但這有助於展示我們如何在實際情況下克服上面提出的挑戰。

實現

很好,現在我們已經介紹了 API 定義的開放標準和背景,讓我們來實現一個 Express 待辦事項應用,演示怎麼解決前面的挑戰。

我們將使用 Express 庫 express-openapi 的 OpenAPI。請注意,這個庫提供的高級功能(響應驗證、認證、中間件設置......)超出了本文的範圍。

你可以在這個倉庫中找到演示的完整代碼。

  1. 初始化一個 Express 框架,並初始化一個 Git 倉庫:
npx express-generator --no-view --git todo-app
cd ./todo-app
git init
git add .; git commit -m "Initial commit";
  1. 將 express-openapi引入我們的程序:

npm i express-openapi -s

// ./app.js

...

app.listen(3030);

...

// OpenAPI routes
initialize({
  app,
  apiDoc: require("./api/api-doc"),
  paths: "./api/paths",
});

module.exports = app;
  1. 添加 OpenAPI 基礎模型。

請注意,模型中定義了 Todo 的類型,將在路由處理程序中引用。

// ./api/api-doc.js

const apiDoc = {
  swagger: "2.0",
  basePath: "/",
  info: {
    title: "Todo app API.",
    version: "1.0.0",
  },
  definitions: {
    Todo: {
      type: "object",
      properties: {
        id: {
          type: "number",
        },
        message: {
          type: "string",
        },
      },
      required: ["id""message"],
    },
  },
  paths: {},
};

module.exports = apiDoc;
  1. 添加路由處理程序。

每個處理程序都聲明它支持哪些操作(GET、POST ...),對每個操作的回調,以及該處理程序的 apiDoc OpenAPI 模型。

// ./api/paths/todos/index.js
module.exports = function () {
  let operations = {
    GET,
    POST,
    PUT,
    DELETE,
  };

  function GET(req, res, next) {
    res.status(200).json([
      { id: 0, message: "First todo" },
      { id: 1, message: "Second todo" },
    ]);
  }

  function POST(req, res, next) {
    console.log(`About to create todo: ${JSON.stringify(req.body)}`);
    res.status(201).send();
  }

  function PUT(req, res, next) {
    console.log(`About to update todo id: ${req.query.id}`);
    res.status(200).send();
  }

  function DELETE(req, res, next) {
    console.log(`About to delete todo id: ${req.query.id}`);
    res.status(200).send();
  }

  GET.apiDoc = {
    summary: "Fetch todos.",
    operationId: "getTodos",
    responses: {
      200: {
        description: "List of todos.",
        schema: {
          type: "array",
          items: {
            $ref"#/definitions/Todo",
          },
        },
      },
    },
  };

  POST.apiDoc = {
    summary: "Create todo.",
    operationId: "createTodo",
    consumes: ["application/json"],
    parameters: [
      {
        in: "body",
        name: "todo",
        schema: {
          $ref"#/definitions/Todo",
        },
      },
    ],
    responses: {
      201: {
        description: "Created",
      },
    },
  };

  PUT.apiDoc = {
    summary: "Update todo.",
    operationId: "updateTodo",
    parameters: [
      {
        in: "query",
        name: "id",
        required: true,
        type: "string",
      },
      {
        in: "body",
        name: "todo",
        schema: {
          $ref"#/definitions/Todo",
        },
      },
    ],
    responses: {
      200: {
        description: "Updated ok",
      },
    },
  };

  DELETE.apiDoc = {
    summary: "Delete todo.",
    operationId: "deleteTodo",
    consumes: ["application/json"],
    parameters: [
      {
        in: "query",
        name: "id",
        required: true,
        type: "string",
      },
    ],
    responses: {
      200: {
        description: "Delete",
      },
    },
  };

  return operations;
};
  1. 添加自動生成的文檔,swagger-ui-express:
npm i swagger-ui-express -s
// ./app.js

...

// OpenAPI UI
app.use(
  "/api-documentation",
  swaggerUi.serve,
  swaggerUi.setup(null, {
    swaggerOptions: {
      url: "http://localhost:3030/api-docs",
    },
  })
);

module.exports = app;

這就是我們最終獲得的效果:

這個 SwaggerUi 是自動生成的,你可以在 http://localhost:3030/api-documentation 訪問它。

🎉 恭喜!

當你進行到文章的這裏時,你應該創建好了一個完全可運行的 Express 應用程序,其與 OpenAPI 完全集成。

現在,通過使用在 http://localhost:3030/api-docs 中定義的模型,我們可以輕鬆生成測試、模擬服務器、類型,甚至客戶端!

總結

我們只是淺淺涉獵了 OpenAPI 所能做到的事情。但是我希望這篇文章能夠讓你瞭解標準 API 定義模式是如何在可見性、測試、文檔和整體置信度方面幫助構建 REST API 的。

謝謝你看到最後,happy coding!


原文鏈接:https://www.freecodecamp.org/news/how-to-build-explicit-apis-with-openapi/

作者:Alain Perkaz

譯者:HeZean

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/igVQvIllO-KqKlXqoz8MbA