Encore.ts 是 TypeScript 的開源後端框架。本指南將引導您了解如何將Express.js應用程式遷移到Encore.ts,以獲得類型安全的 API 和 9 倍的效能提升。

為什麼要遷移到 Encore.ts?

Express.js 是建立簡單 API 的絕佳選擇,但隨著應用程式的成長,您可能會遇到限制。 Express.js 有一個大型社區,提供許多插件和中間件來解決這些限制。然而,嚴重依賴插件可能會導致很難找到適合您的用例的工具。這也意味著您將需要維護大量依賴項。

Encore.ts 是一個開源框架,旨在更輕鬆地使用 TypeScript 建立健全且類型安全的後端。 Encore.ts 有0 個 npm 依賴項,在建置時考慮了效能,並且具有許多用於建置生產就緒後端的內建功能。您可以在任何接受 Docker 容器的託管服務上自助提供Encore.ts 應用程式,或使用Encore Cloud Platform來完全自動化您的 DevOps 和基礎設施。

表現

Encore.ts 有自己的高效能執行時,帶有用 Rust 編寫的多執行緒非同步事件循環。 Encore Runtime 處理所有 I/O,例如接受和處理傳入的 HTTP 請求。它作為一個完全獨立的事件循環執行,利用底層硬體支援的盡可能多的執行緒。結果是 Encore.ts 的執行速度比 Express.js快 9 倍

Encore.ts 每秒處理的請求比 Express.js 多 9 倍

每秒 encore.ts 請求數

Encore.ts 的反應延遲比 Express.js 減少 80%

encore.ts 延遲

https://dev.to/encore/encorets-9x-faster-than-expressjs-3x-faster-than-bun-zod-4boe

內建優勢

使用 Encore.ts 時,您可以獲得許多內建功能,而無需安裝任何額外的依賴項

local dev 儀表板

https://www.youtube.com/watch?v=Da\_jHj6bLac

遷移指南

下面我們概述了兩種可用於將現有 Express.js 應用程式遷移到 Encore.ts 的主要策略。選擇最適合您的情況和應用的策略。

堆高機遷移(快速入門)

當您想要快速遷移到 Encore.ts 並且不需要所有功能時,您可以使用堆高機遷移策略。這種方法透過將現有的 HTTP 路由器包裝在一個包羅萬象的處理程序中,一次將整個應用程式轉移到 Encore.ts。

方法的好處

  • 您可以使用 Encore.ts 快速啟動並執行您的應用程式,並開始將功能轉移到 Encore.ts,而應用程式的其餘部分仍保持不變。

  • 您將立即看到部分效能提升,因為 HTTP 層現在在 Encore Rust 執行時上執行。但要獲得全部效能優勢,您需要開始使用 Encore 的API 聲明基礎架構聲明

方法的缺點

  • 由於所有請求都將透過 catch-all 處理程序進行代理,因此您將無法從分散式追蹤中獲得所有好處。

  • 在您開始將服務和 API 移至 Encore.ts 之前,自動產生的架構圖和 API 文件將無法向您展示應用程式的全貌。

  • 在開始在 Encore.ts 中定義 API 之前,您將無法使用 API 用戶端產生功能。

1.安裝encore

如果這是您第一次使用 Encore,您首先需要安裝執行本機開發環境的 CLI。使用適合您系統的命令:

  • macOS: brew install encoredev/tap/encore

  • Linux: curl -L https://encore.dev/install.sh | bash

  • Windows: iwr https://encore.dev/install.ps1 | iex

安裝文件

2. 將 Encore.ts 加入您的專案中

npm i encore.dev

3. 初始化 Encore 應用程式

在專案目錄中,執行以下命令來建立 Encore 應用程式:

encore app init

這將在專案的根目錄中建立一個encore.app檔案。

4. 配置 tsconfig.json

在專案根目錄中的tsconfig.json檔案中,新增以下內容:

{
   "compilerOptions": {
      "paths": {
         "~encore/*": [
            "./encore.gen/*"
         ]
      }
   }
}

當 Encore.ts 解析您的程式碼時,它將專門尋找~encore/*導入。

5. 定義 Encore.ts 服務

使用 Encore.ts 執行應用程式時,您至少需要一項Encore 服務。除此之外,Encore.ts 對如何建立程式碼沒有固執己見,您可以自由地採用整體或微服務方法。在我們的應用程式結構文件中了解更多。

在應用程式的根目錄中,新增一個名為encore.service.ts的檔案。該檔案必須透過呼叫從encore.dev/service導入的new Service來匯出服務實例:

import {Service} from "encore.dev/service";

export default new Service("my-service");

Encore 會將此目錄及其所有子目錄視為服務的一部分。

6. 為 HTTP 路由器建立一個包羅萬象的處理程序

現在,讓我們將現有的應用程式路由器掛載到Raw 端點下,這是一種 Encore API 端點類型,可讓您存取底層 HTTP 請求。

這是一個基本的程式碼範例:

import { api, RawRequest, RawResponse } from "encore.dev/api";
import express, { request, response } from "express";

Object.setPrototypeOf(request, RawRequest.prototype);
Object.setPrototypeOf(response, RawResponse.prototype);

const app = express();

app.get('/foo', (req: any, res) => {
  res.send('Hello World!')
})

export const expressApp = api.raw(
  { expose: true, method: "*", path: "/!rest" },
  app,
);

透過以這種方式安裝現有的應用程式路由器,它將作為所有 HTTP 請求和回應的包羅萬象的處理程序。

7. 在本地執行您的應用程式

現在您可以使用encore run命令在本地執行 Express.js 應用程式。

後續步驟:逐步轉移到 Encore.ts 以獲取所有好處

現在您可以使用 Encore 的API 聲明逐步分解特定端點,並引入資料庫和 cron 作業等的基礎設施聲明。有關更多詳細訊息,請參閱逐個功能遷移部分。您最終將能夠刪除 Express.js 作為依賴項並完全在 Encore.ts 上執行您的應用程式。

有關將現有後端遷移到 Encore.ts 的更多想法,請查看我們的一般遷移指南。您也可以加入 Discord提出問題並會見其他 Encore 開發人員。

完全遷移

此方法旨在以 Encore.ts 完全取代應用程式對 Express.js 的依賴,從而解鎖 Encore.ts 的所有功能和效能。

在下一部分中,您將找到逐一功能的遷移指南,以幫助您了解重構細節。

方法的好處

  • 獲得 Encore.ts 的所有優勢,例如分散式追蹤和架構圖。

  • 充分發揮 Encore.ts 的效能優勢 - 比 Express.js快 9 倍

方法的缺點

  • 與堆高機遷移策略相比,這種方法可能需要更多的時間和精力。

逐一功能遷移

請參閱 GitHub 上的Express.js 與 Encore.ts 比較範例,以了解此功能比較中的所有程式碼片段。

透過 Express.js,您可以使用app.getapp.postapp.putapp.delete函數建立 API。這些函數採用路徑和回呼函數。然後,您可以使用reqres物件來處理請求和回應。

透過 Encore.ts,您可以使用api函數建立 API。此函數採用一個選項物件和一個回呼函數。與 Express.js 相比,主要區別在於 Encore.ts 是類型安全的,這意味著您可以在回呼函數中定義請求和回應架構。然後,您傳回與回應模式相符的物件。如果您需要在較低的抽象層級進行操作,Encore 支援定義原始端點,讓您可以存取底層 HTTP 請求。請參閱我們的API 架構文件以了解更多資訊。

Express.js

import express, {Request, Response} from "express";

const app: Express = express();

// GET request with dynamic path parameter
app.get("/hello/:name", (req: Request, res: Response) => {
  const msg = `Hello ${req.params.name}!`;
  res.json({message: msg});
})

// GET request with query string parameter
app.get("/hello", (req: Request, res: Response) => {
  const msg = `Hello ${req.query.name}!`;
  res.json({message: msg});
});

// POST request example with JSON body
app.post("/order", (req: Request, res: Response) => {
  const price = req.body.price;
  const orderId = req.body.orderId;
  // Handle order logic
  res.json({message: "Order has been placed"});
});

encore.ts

import {api, Query} from "encore.dev/api";

// Dynamic path parameter :name
export const dynamicPathParamExample = api(
  {expose: true, method: "GET", path: "/hello/:name"},
  async ({name}: { name: string }): Promise<{ message: string }> => {
    const msg = `Hello ${name}!`;
    return {message: msg};
  },
);

interface RequestParams {
  // Encore will now automatically parse the query string parameter
  name?: Query<string>;
}

// Query string parameter ?name
export const queryStringExample = api(
  {expose: true, method: "GET", path: "/hello"},
  async ({name}: RequestParams): Promise<{ message: string }> => {
    const msg = `Hello ${name}!`;
    return {message: msg};
  },
);

interface OrderRequest {
  price: string;
  orderId: number;
}

// POST request example with JSON body
export const order = api(
  {expose: true, method: "POST", path: "/order"},
  async ({price, orderId}: OrderRequest): Promise<{ message: string }> => {
    // Handle order logic
    console.log(price, orderId)

    return {message: "Order has been placed"};
  },
);

// Raw endpoint
export const myRawEndpoint = api.raw(
  {expose: true, path: "/raw", method: "GET"},
  async (req, resp) => {
    resp.writeHead(200, {"Content-Type": "text/plain"});
    resp.end("Hello, raw world!");
  },
);

Express.js 沒有對建立微服務或服務間通訊的內建支援。您很可能會使用fetch或等效的方法來呼叫其他服務。

使用 Encore.ts,呼叫另一個服務就像呼叫本地函數一樣,具有完整的類型安全性。在底層,Encore.ts 會將此函數呼叫轉換為實際的服務到服務 HTTP 呼叫,從而為每個呼叫產生追蹤資料。請參閱我們的服務到服務通訊文件以了解更多資訊。

Express.js

import express, {Request, Response} from "express";

const app: Express = express();

app.get("/save-post", async (req: Request, res: Response) => {
  try {
    // Calling another service using fetch
    const resp = await fetch("https://another-service/posts", {
      method: "POST",
      headers: {"Content-Type": "application/json"},
      body: JSON.stringify({
        title: req.query.title,
        content: req.query.content,
      }),
    });
    res.json(await resp.json());
  } catch (e) {
    res.status(500).json({error: "Could not save post"});
  }
});

encore.ts

import {api} from "encore.dev/api";
import {anotherService} from "~encore/clients";

export const microserviceCommunication = api(
  {expose: true, method: "GET", path: "/call"},
  async (): Promise<{ message: string }> => {
    // Calling the foo endpoint in anotherService
    const fooResponse = await anotherService.foo();

    const msg = `Data from another service ${fooResponse.data}!`;
    return {message: msg};
  },
);

在 Express.js 中,您可以建立一個中間件函數來檢查使用者是否經過驗證。然後,您可以在路由中使用此中間件功能來保護它們。您必須為每個需要身份驗證的路由指定中間件功能。

對於 Encore.ts,當使用auth: true定義 API 時,您必須在應用程式中定義驗證處理程序。身份驗證處理程序負責檢查傳入請求以確定哪些使用者經過身份驗證。

驗證處理程序的定義與 API 端點類似,使用從encore.dev/auth導入的authHandler函數。與 API 端點一樣,驗證處理程序以 HTTP 標頭、查詢字串或 cookie 的形式定義它感興趣的請求資訊。

如果請求已成功通過驗證,API 閘道會將身分驗證資料轉送至目標端點。端點可以從~encore/auth模組中的getAuthData函數查詢可用的身份驗證資料。

在我們的身份驗證處理程序文件中了解更多訊息

Express.js

import express, {NextFunction, Request, Response} from "express";

const app: Express = express();

// Auth middleware
function authMiddleware(req: Request, res: Response, next: NextFunction) {
  // TODO: Validate up auth token and verify that this is an authenticated user
  const isInvalidUser = req.headers["authorization"] === undefined;

  if (isInvalidUser) {
    res.status(401).json({error: "invalid request"});
  } else {
    next();
  }
}

// Endpoint that requires auth
app.get("/dashboard", authMiddleware, (_, res: Response) => {
  res.json({message: "Secret dashboard message"});
});

encore.ts

import { api, APIError, Gateway, Header } from "encore.dev/api";
import { authHandler } from "encore.dev/auth";
import { getAuthData } from "~encore/auth";

interface AuthParams {
  authorization: Header<"Authorization">;
}

// The function passed to authHandler will be called for all incoming API call that requires authentication.
export const myAuthHandler = authHandler(
  async (params: AuthParams): Promise<{ userID: string }> => {
    // TODO: Validate up auth token and verify that this is an authenticated user
    const isInvalidUser = params.authorization === undefined;

    if (isInvalidUser) {
      throw APIError.unauthenticated("Invalid user ID");
    }

    return { userID: "user123" };
  },
);

export const gateway = new Gateway({ authHandler: myAuthHandler });

// Auth endpoint example
export const dashboardEndpoint = api(
  // Setting auth to true will require the user to be authenticated
  { auth: true, method: "GET", path: "/dashboard" },
  async (): Promise<{ message: string; userID: string }> => {
    return {
      message: "Secret dashboard message",
      userID: getAuthData()!.userID,
    };
  },
);

Express.js 沒有內建的請求驗證。您必須使用像Zod這樣的函式庫。

對於 Encore.ts,請求頭、查詢參數和正文的驗證是。您為請求物件提供架構,如果請求負載與該架構不匹配,API 將傳回 400 錯誤。請參閱我們的API 架構文件以了解更多資訊。

Express.js

import express, {NextFunction, Request, Response} from "express";
import {z, ZodError} from "zod";

const app: Express = express();

// Request validation middleware
function validateData(schemas: {
  body: z.ZodObject<any, any>;
  query: z.ZodObject<any, any>;
  headers: z.ZodObject<any, any>;
}) {
  return (req: Request, res: Response, next: NextFunction) => {
    try {
      // Validate headers
      schemas.headers.parse(req.headers);

      // Validate request body
      schemas.body.parse(req.body);

      // Validate query params
      schemas.query.parse(req.query);

      next();
    } catch (error) {
      if (error instanceof ZodError) {
        const errorMessages = error.errors.map((issue: any) => ({
          message: `${issue.path.join(".")} is ${issue.message}`,
        }));
        res.status(400).json({error: "Invalid data", details: errorMessages});
      } else {
        res.status(500).json({error: "Internal Server Error"});
      }
    }
  };
}

// Request body validation schemas
const bodySchema = z.object({
  someKey: z.string().optional(),
  someOtherKey: z.number().optional(),
  requiredKey: z.array(z.number()),
  nullableKey: z.number().nullable().optional(),
  multipleTypesKey: z.union([z.boolean(), z.number()]).optional(),
  enumKey: z.enum(["John", "Foo"]).optional(),
});

// Query string validation schemas
const queryStringSchema = z.object({
  name: z.string().optional(),
});

// Headers validation schemas
const headersSchema = z.object({
  "x-foo": z.string().optional(),
});

// Request validation example using Zod
app.post(
  "/validate",
  validateData({
    headers: headersSchema,
    body: bodySchema,
    query: queryStringSchema,
  }),
  (_: Request, res: Response) => {
    res.json({message: "Validation succeeded"});
  },
);

encore.ts

import {api, Header, Query} from "encore.dev/api";

enum EnumType {
  FOO = "foo",
  BAR = "bar",
}

// Encore.ts automatically validates the request schema and returns and error
// if the request does not match the schema.
interface RequestSchema {
  foo: Header<"x-foo">;
  name?: Query<string>;

  someKey?: string;
  someOtherKey?: number;
  requiredKey: number[];
  nullableKey?: number | null;
  multipleTypesKey?: boolean | number;
  enumKey?: EnumType;
}

// Validate a request
export const schema = api(
  {expose: true, method: "POST", path: "/validate"},
  (data: RequestSchema): { message: string } => {
    console.log(data);
    return {message: "Validation succeeded"};
  },
);

在 Express.js 中,您要么拋出錯誤(導致 500 回應),要么使用status函數設定回應的狀態程式碼。

在 Encore.ts 中拋出錯誤將導致 500 回應。您也可以使用APIError類別傳回特定的錯誤程式碼。請參閱我們的API 錯誤文件以了解更多資訊。

Express.js

import express, {Request, Response} from "express";

const app: Express = express();

// Default error handler
app.get("/broken", (req, res) => {
  throw new Error("BROKEN"); // This will result in a 500 error
});

// Returning specific error code
app.get("/get-user", (req: Request, res: Response) => {
  const id = req.query.id || "";
  if (id.length !== 3) {
    res.status(400).json({error: "invalid id format"});
  }
  // TODO: Fetch something from the DB
  res.json({user: "Simon"});
});

encore.ts

import {api, APIError} from "encore.dev/api"; // Default error handler

// Default error handler
export const broken = api(
  {expose: true, method: "GET", path: "/broken"},
  async (): Promise<void> => {
    throw new Error("This is a broken endpoint"); // This will result in a 500 error
  },
);

// Returning specific error code
export const brokenWithErrorCode = api(
  {expose: true, method: "GET", path: "/broken/:id"},
  async ({id}: { id: string }): Promise<{ user: string }> => {
    if (id.length !== 3) {
      throw APIError.invalidArgument("invalid id format");
    }
    // TODO: Fetch something from the DB
    return {user: "Simon"};
  },
);

Express.js 有一個內建的中間件功能來提供靜態檔案。您可以使用express.static函數來提供特定目錄中的檔案。

Encore.ts 也內建支援使用api.static方法提供靜態檔案。

這些檔案直接從 Encore.ts Rust 執行時期提供。這意味著執行零 JavaScript 程式碼來提供文件,從而釋放 Node.js 執行時以專注於執行業務邏輯。這大大加快了靜態檔案服務的速度,

以及改善 API 端點的延遲。在我們的靜態資產文件中了解更多訊息

Express.js

import express from "express";

const app: Express = express();

app.use("/assets", express.static("assets")); // Serve static files from the assets directory

encore.ts

import { api } from "encore.dev/api";

export const assets = api.static(
  { expose: true, path: "/assets/*path", dir: "./assets" },
);

Express.js 內建了渲染模板的支援。

透過 Encore.ts,您可以使用api.raw函數來提供 HTML 模板,在本例中我們使用 Handlebars.js,但您可以使用您喜歡的任何模板引擎。在我們的原始端點文件中了解更多訊息

Express.js

import express, {Request, Response} from "express";

const app: Express = express();

app.set("view engine", "pug"); // Set view engine to Pug

// Template engine example. This will render the index.pug file in the views directory
app.get("/html", (_, res) => {
  res.render("index", {title: "Hey", message: "Hello there!"});
});

encore.ts

import {api} from "encore.dev/api";
import Handlebars from "handlebars";

const html = `
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8"/>
  <link rel="stylesheet" href="/assets/styles.css">
</head>
<body>
<h1>Hello {{name}}!</h1>
</body>
</html>
`;

// Making use of raw endpoints to serve dynamic templates.
// https://encore.dev/docs/ts/primitives/services-and-apis#raw-endpoints
export const serveHTML = api.raw(
  {expose: true, path: "/html", method: "GET"},
  async (req, resp) => {
    const template = Handlebars.compile(html);

    resp.setHeader("Content-Type", "text/html");
    resp.end(template({name: "Simon"}));
  },
);

Express.js 沒有內建的測試支援。您可以使用Vitest

超級測試

使用 Encore.ts,您可以在測試中直接呼叫 API 端點,就像其他函數一樣。然後,您可以使用encore test指令執行測試。請參閱我們的測試文件以了解更多資訊。

Express.js

import {describe, expect, test} from "vitest";
import request from "supertest";
import express from "express";
import getRequestExample from "../get-request-example";

/**
 * We need to add the supertest library to make fake HTTP requests to the Express.js app without having to
 * start the server. We also use the vitest library to write tests.
 */
describe("Express App", () => {
  const app = express();
  app.use("/", getRequestExample);

  test("should respond with a greeting message", async () => {
    const response = await request(app).get("/hello/John");
    expect(response.status).to.equal(200);
    expect(response.body).to.have.property("message");
    expect(response.body.message).to.equal("Hello John!");
  });
});

encore.ts

import {describe, expect, test} from "vitest";
import {dynamicPathParamExample} from "../get-request-example";

// This test suite demonstrates how to test an Encore route.
// Run tests using the `encore test` command.
describe("Encore app", () => {
  test("should respond with a greeting message", async () => {
    // You can call the Encore.ts endpoint directly in your tests,
    // just like any other function.
    const resp = await dynamicPathParamExample({name: "world"});
    expect(resp.message).toBe("Hello world!");
  });
});

Express.js 沒有內建資料庫支援。您可以使用pg-promise等程式庫連接到 PostgreSQL 資料庫,但您也必須管理不同環境的 Docker Compose 檔案。

使用 Encore.ts,您可以透過匯入encore.dev/storage/sqldb並呼叫new SQLDatabase來建立資料庫,並將結果指派給頂層變數。

資料庫模式是透過建立遷移檔案來定義的。每個遷移都按順序執行,並表示資料庫架構相對於上一個遷移的變更。

Encore.ts 自動配置資料庫以滿足您的應用程式的需求。 Encore.ts 根據環境以適當的方式配置資料庫。在本機執行時,Encore 使用 Docker 建立資料庫叢集。在雲端中,這取決於環境類型:

要查詢資料,請使用.query.queryRow方法。若要插入資料或進行不傳回任何行的資料庫查詢,請使用.exec

在我們的資料庫文件中了解更多。

encore.ts

資料庫ts

import {api} from "encore.dev/api";
import {SQLDatabase} from "encore.dev/storage/sqldb";

// Define a database named 'users', using the database migrations in the "./migrations" folder.
// Encore automatically provisions, migrates, and connects to the database.
export const DB = new SQLDatabase("users", {
  migrations: "./migrations",
});

interface User {
  name: string;
  id: number;
}

// Get one User from DB
export const getUser = api(
  {expose: true, method: "GET", path: "/user/:id"},
  async ({id}: { id: number }): Promise<{ user: User | null }> => {
    const user = await DB.queryRow<User>`
        SELECT name
        FROM users
        WHERE id = ${id}
    `;

    return {user};
  },
);

// Add User from DB
export const addUser = api(
  { expose: true, method: "POST", path: "/user" },
  async ({ name }: { name: string }): Promise<void> => {
    await DB.exec`
        INSERT INTO users (name)
        VALUES (${name})
    `;
    return;
  },
);

遷移/1_create_tables.up.sql

CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL UNIQUE
);

Express.js 沒有內建的日誌記錄支援。您可以使用像Winston這樣的庫來記錄訊息。

Encore.ts 提供對結構化日誌記錄的內建支持,它將自由格式的日誌訊息與結構化且類型安全的鍵值對相結合。日誌記錄與內建的分散式追蹤功能集成,所有日誌都會自動包含在活動追蹤中。在我們的日誌記錄文件中了解更多。

encore.ts

import log from "encore.dev/log";

log.error(err, "something went terribly wrong!");
log.info("log message", { is_subscriber: true });

其他相關文章

https://dev.to/encore/encorets-17x-faster-cold-starts-than-nestjs-fastify-27cg

https://dev.to/encore/how-to-let-chatgpt-call-functions-in-your-app-27j8

https://dev.to/encore/encorets-9x-faster-than-expressjs-3x-faster-than-bun-zod-4boe


原文出處:https://dev.to/encore/how-to-make-your-expressjs-apis-9x-faster-with-encorets-1ke2


共有 0 則留言