如何让你的 Express 应用速度提高 9 倍(并且类型安全)
💡 本指南向您展示如何将现有的 Express.js 应用程序迁移到Encore.ts(TypeScript 的开源后端框架),以实现9 倍的性能提升。
为什么要迁移?
Express.js 非常适合简单的 API,但随着应用规模的扩大,你可能会面临一些限制。虽然庞大的 Express.js 社区提供了插件和中间件来提供帮助,但过度依赖它们会增加复杂性和依赖性。
Encore.ts 是一个 TypeScript 框架,专为稳健、类型安全的后端而设计。它无需 npm 依赖,注重性能,并拥有丰富的内置功能,简化了生产级应用的构建。您可以使用任何兼容 Docker 的托管解决方案自行托管 Encore.ts 应用,也可以使用 Encore云平台在您自己的 AWS 或 GCP 帐户中实现自动化 DevOps 和基础设施管理。
表现
Encore.ts 拥有自己的高性能 Rust 运行时,可使用Napi与 Node.js 集成。
Encore.ts 使用 Rust 语言编写的多线程异步事件循环扩展了 Node.js,处理所有 I/O 操作,例如接受和处理传入的 HTTP 请求。它以完全独立的事件循环运行,利用底层硬件支持的线程数量,从而显著提升了单线程 JavaScript 的性能。Encore.ts每秒
处理的请求数比 Express.js 多 9 倍,比 Elysia 和 Hono 多 3 倍。
您可以在此处查看基准代码。
内在优势
使用 Encore.ts 时,您可以获得许多内置功能,而无需安装任何其他依赖项:
类型安全的 API 模式 | CORS 处理 | 结构化日志记录 | 验证 |
发布/订阅集成 | 机密管理 | 基础设施整合 | 数据库集成 |
架构图 | 本地开发仪表板 | 服务目录 | TypeScript 原生 |
调试 | 错误处理 | 多线程 | 请求验证 |
追踪 | API 客户端生成 | 自动本地基础设施 | 自动化测试 |
本地开发仪表板
迁移指南
下面我们概述了两种将现有 Express.js 应用程序迁移到 Encore.ts 的主要策略。请选择最适合您情况和应用的策略。
叉车迁移(快速启动)
如果您希望快速迁移到 Encore.ts,并且不需要所有功能,可以使用叉车迁移策略。此方法通过将现有的 HTTP 路由器包装在一个 catch-all 处理程序中,一次性将整个应用程序迁移到 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
- 视窗:
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
。该文件必须通过调用 导出一个服务实例,new Service
并从 导入encore.dev/service
:
import {Service} from "encore.dev/service";
export default new Service("my-service");
Encore 将把该目录及其所有子目录视为服务的一部分。
6. 为 HTTP 路由器创建一个 catch-all 处理程序
现在让我们将您现有的应用路由器安装在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 作业等基础设施声明。这将使 Encore.ts 能够理解您的应用程序并解锁所有 Encore.ts 功能。有关更多详细信息,请参阅逐个功能迁移部分。最终,您将能够移除 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 示例的比较,了解此功能比较中的所有代码片段。
蜜蜂
app.get
使用 Express.js,您可以使用、app.post
、app.put
、函数创建 API app.delete
。这些函数接受路径和回调函数作为参数。然后,您可以使用req
和res
对象来处理请求和响应。
使用 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"});
});
安可
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"});
}
});
安可
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 时,当使用 定义 API 时auth: true
,必须在应用程序中定义身份验证处理程序。身份验证处理程序负责检查传入的请求,以确定哪些用户已通过身份验证。
身份验证处理程序的定义与 API 端点类似,使用authHandler
从 导入的函数encore.dev/auth
。与 API 端点一样,身份验证处理程序以 HTTP 标头、查询字符串或 Cookie 的形式定义其感兴趣的请求信息。
如果请求已成功通过身份验证,API 网关会将身份验证数据转发到目标端点。该端点可以从模块getAuthData
提供的函数中查询可用的身份验证数据~encore/auth
。
在我们的Auth Handler 文档中了解更多信息
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"});
});
安可
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"});
},
);
安可
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"});
});
安可
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
安可
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!"});
});
安可
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和
Supertest等库。
使用 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!");
});
});
安可
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.dev/storage/sqldb
使用 Encore.ts,您可以通过导入和调用来创建数据库new SQLDatabase
,并将结果分配给顶级变量。
数据库模式是通过创建迁移文件来定义的。每次迁移都按顺序运行,并表达与上次迁移相比数据库模式的变化。
Encore.ts 会自动配置数据库以满足您的应用程序需求。Encore.ts 会根据环境以适当的方式配置数据库。在本地运行时,Encore 会使用 Docker 创建数据库集群。在云端,则取决于环境类型:
要查询数据,请使用.query
或.queryRow
方法。要插入数据或进行不返回任何行的数据库查询,请使用.exec
。
在我们的数据库文档中了解更多信息。
安可
db.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 内置了结构化日志记录支持,它将自由格式的日志消息与结构化且类型安全的键值对相结合。日志记录与内置的分布式跟踪功能集成,所有日志都会自动包含在活动跟踪中。请参阅我们的日志记录文档了解更多信息。
安可
import log from "encore.dev/log";
log.error(err, "something went terribly wrong!");
log.info("log message", { is_subscriber: true });
其他相关文章


Encore.ts — 冷启动速度比 NestJS 和 Fastify 快 17 倍
Marcus Kohlberg 为 Encore 演唱 ・ 9 月 4 日

