第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
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,处理函数没执行。
四、接入数据库
迁移
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");
});
});
注意这个测试的成色:shorten 与 get 是真函数,背后是真数据库(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" 放烟花——不影响生产,可以试。)
八、可选扩展(自练)
官方教程到此为止。几个顺手的强化方向,全部是前面章节的组合:
- 真跳转:加 GET /:id 的 api.raw 端点返回 302 重定向(resp.writeHead(302, { Location: row.original_url }));
- 防刷:IntKeyspace.increment 按来源 IP 限流(第 9 章);
- 访问统计:跳转时发布 link-visited 事件,独立订阅累计点击数(第 7 章);
- 过期清理:表加 created_at,CronJob 每天清 90 天前的记录(第 8 章);
- 短链校验:ShortenParams.url 加 IsURL 校验器(第 4 章)——官方教程没加,生产必须加。
九、常见误区
- 忘了 mkdir url/migrations 就声明数据库——迁移目录不存在,构建报错。
- db.exec 的模板字符串写成普通字符串拼接——注入风险(第 5 章)。
- queryRow 结果不判 null 直接取字段——短链不存在时 undefined 炸在运行时;判空抛 notFound。
- 测试用 npx vitest 跑——没有基础设施绑定。
- infra-config.json 里写明文密码——用 {"$env": "..."} 走环境变量。
十、总结
- 完整闭环 = 服务声明 + 2 个端点 + 1 张迁移 + 1 个测试文件,约 60 行业务代码。
- 数据库从声明到可用零配置:SQLDatabase + 迁移目录 + encore run。
- 错误路径显式化:queryRow 判 null → APIError.notFound。
- 测试直接调函数、打真库,encore test 是唯一正确入口。
- 部署两路线同一套代码:Docker 镜像自托管,或 git push encore 上云。
请继续阅读:第14章:实战二 Uptime 监控系统。
原始资料引用