Agent X-Ray
RuntimeNotesAbout
Notes/代码工程/Encore/第13章

第13章:实战一 —— URL 短链服务

3 分钟 · 更新于 2026-09-02

官方 REST API 教程的完整中文版。麻雀虽小:一个服务、一个数据库、两个端点、一组测试、两条部署路径——前 5 章的知识在 100 行代码里全部落地。


一、目标与蓝图

做一个 URL 缩短服务:

  • POST /url:提交长链接,返回短 id;
  • GET /url/:id:用短 id 取回原链接;
  • 数据落 Postgres;带集成测试;可部署。
text
url 服务
├── POST /url      ──▶ 生成 id ──▶ INSERT
└── GET  /url/:id  ──▶ SELECT ──▶ 404 或返回

二、创建应用与服务

bash
encore app create   # 选 Empty app,命名 url-shortener
cd url-shortener
mkdir url
typescript
// url/encore.service.ts
import { Service } from "encore.dev/service";
export default new Service("url");

三、第一个端点:缩短 URL

typescript
// url/url.ts
import { api } from "encore.dev/api";
import { randomBytes } from "node:crypto";

interface URL {
  id: string;  // 短链 id
  url: string; // 原始完整 URL
}

interface ShortenParams {
  url: string; // 待缩短的 URL
}

// 缩短一个 URL
export const shorten = api(
  { method: "POST", path: "/url", expose: true },
  async ({ url }: ShortenParams): Promise<URL> => {
    const id = randomBytes(6).toString("base64url");
    return { id, url };
  },
);

randomBytes(6).toString("base64url") 生成 8 字符的 URL 安全随机 id。启动并验证:

bash
encore run
bash
curl http://localhost:4000/url -d '{"url": "https://encore.dev"}'
# → {"id": "5cJpBVRp", "url": "https://encore.dev"}

此时打开 localhost:9400:Service Catalog 已有 url.shorten 的文档,Tracing 里能看到刚才这次请求。

这一步复习了什么 类型即校验(第 4 章):curl -d '{}' 试试——缺 url 字段直接 400,处理函数没执行。

四、接入数据库

迁移

bash
mkdir url/migrations
sql
-- url/migrations/001_create_tables.up.sql
CREATE TABLE url (
	id TEXT PRIMARY KEY,
	original_url TEXT NOT NULL
);

声明数据库并写入

typescript
// url/url.ts(更新头部与 shorten)
import { api } from "encore.dev/api";
import { SQLDatabase } from "encore.dev/storage/sqldb";
import { randomBytes } from "node:crypto";

// 'url' 数据库,迁移在 ./migrations
const db = new SQLDatabase("url", { migrations: "./migrations" });

export const shorten = api(
  { method: "POST", path: "/url", expose: true },
  async ({ url }: ShortenParams): Promise<URL> => {
    const id = randomBytes(6).toString("base64url");
    await db.exec`
      INSERT INTO url (id, original_url)
      VALUES (${id}, ${url})
    `;
    return { id, url };
  },
);

重跑 encore run:框架发现新数据库 → Docker 拉起 Postgres → 执行迁移。验证落库:

bash
curl http://localhost:4000/url -d '{"url": "https://encore.dev"}'
encore db shell url
sql
select * from url;
--     id     |    original_url
-- -----------+--------------------
--  zr6RmZc4  | https://encore.dev

五、查询端点与 404

typescript
// url/url.ts(新增;注意 import 增加 APIError)
import { api, APIError } from "encore.dev/api";

// 按 id 取回原始 URL
export const get = api(
  { expose: true, auth: false, method: "GET", path: "/url/:id" },
  async ({ id }: { id: string }): Promise<URL> => {
    const row = await db.queryRow`
      SELECT original_url FROM url WHERE id = ${id}
    `;
    if (!row) throw APIError.notFound("url not found");
    return { id, url: row.original_url };
  },
);
bash
curl http://localhost:4000/url/zr6RmZc4
# → {"id": "zr6RmZc4", "url": "https://encore.dev"}

curl -i http://localhost:4000/url/not-exist
# → HTTP 404, {"code": "not_found", "message": "url not found", ...}

六、测试

bash
npm i --save-dev vitest
json
// package.json 增加
"scripts": {
  "test": "vitest"
}
typescript
// url/url.test.ts
import { describe, expect, test } from "vitest";
import { get, shorten } from "./url";

describe("shorten", () => {
  test("getting a shortened url should give back the original", async () => {
    const resp = await shorten({ url: "https://example.com" });
    const url = await get({ id: resp.id });
    expect(url.url).toBe("https://example.com");
  });
});
bash
encore test

注意这个测试的成色:shortenget真函数,背后是真数据库(encore test 自动建的隔离测试库)——写入再读出的闭环没有一个 mock。这是 Encore 测试哲学的缩影(第 15 章展开)。

必须 encore test,不要裸跑 vitest 裸 vitest 拿不到运行时与基础设施绑定,数据库调用直接失败。这条要写进项目规则——AI 助手尤其爱裸跑 vitest(第 20 章)。

七、部署

路线 A:自托管 Docker

json
// infra-config.json
{
  "$schema": "https://encore.dev/schemas/infra.schema.json",
  "sql_servers": [
    {
      "host": "my-db-host:5432",
      "databases": {
        "url": {
          "username": "my-db-owner",
          "password": { "$env": "DB_PASSWORD" }
        }
      }
    }
  ]
}
bash
encore build docker url-shortener:v1.0
docker run -p 8080:8080 -e DB_PASSWORD=xxx url-shortener:v1.0

镜像是标准 OCI 镜像,K8s / Cloud Run / 裸机随意(细节第 16 章)。

路线 B:Encore Cloud

bash
git add -A . && git commit -m 'Initial commit'
git push encore        # 触发云端构建与部署

部署完成后 Cloud Dashboard 可看环境、追踪与日志。(官方教程的最后一步是在 Dashboard 按 Cmd/Ctrl+K 输入 "fireworks" 放烟花——不影响生产,可以试。)

八、可选扩展(自练)

官方教程到此为止。几个顺手的强化方向,全部是前面章节的组合:

  1. 真跳转:加 GET /:idapi.raw 端点返回 302 重定向(resp.writeHead(302, { Location: row.original_url }));
  2. 防刷IntKeyspace.increment 按来源 IP 限流(第 9 章);
  3. 访问统计:跳转时发布 link-visited 事件,独立订阅累计点击数(第 7 章);
  4. 过期清理:表加 created_at,CronJob 每天清 90 天前的记录(第 8 章);
  5. 短链校验ShortenParams.urlIsURL 校验器(第 4 章)——官方教程没加,生产必须加。

九、常见误区

  1. 忘了 mkdir url/migrations 就声明数据库——迁移目录不存在,构建报错。
  2. db.exec 的模板字符串写成普通字符串拼接——注入风险(第 5 章)。
  3. queryRow 结果不判 null 直接取字段——短链不存在时 undefined 炸在运行时;判空抛 notFound
  4. 测试用 npx vitest 跑——没有基础设施绑定。
  5. infra-config.json 里写明文密码——用 {"$env": "..."} 走环境变量。

十、总结

  1. 完整闭环 = 服务声明 + 2 个端点 + 1 张迁移 + 1 个测试文件,约 60 行业务代码。
  2. 数据库从声明到可用零配置:SQLDatabase + 迁移目录 + encore run
  3. 错误路径显式化:queryRow 判 null → APIError.notFound
  4. 测试直接调函数、打真库,encore test 是唯一正确入口。
  5. 部署两路线同一套代码:Docker 镜像自托管,或 git push encore 上云。

请继续阅读:第14章:实战二 Uptime 监控系统


原始资料引用



本章目录
一、目标与蓝图二、创建应用与服务三、第一个端点:缩短 URL四、接入数据库五、查询端点与 404六、测试七、部署八、可选扩展(自练)九、常见误区十、总结原始资料引用Related Documents
苏ICP备2025204887号-2