freeCodeCamp 后端认证实战:基于 URL Shortener Microservice 挑战规格构建全栈 URL 短链接服务
本篇基于 freeCodeCamp 课程仓库中 URL Shortener Microservice 挑战定义文件,完整拆解这个后端认证项目的 API 契约、官方测试逻辑与关键技术点(body 解析中间件、dns.lookup URL 校验、重定向机制),并给出可复制运行的完整实现方案,帮助你在本地交付一个能通过官方自动测试的全栈短链接微服务。
一、项目定位:它在 freeCodeCamp 课程体系中的位置
URL Shortener Microservice 是 Back-End Development and APIs(后端开发与 API)认证下的第 3 个项目。从课程组织结构文件可以确认它的归属链路:
- 超级块定义 curriculum/structure/superblocks/back-end-development-and-apis.json 列出了该认证的四个 block:
managing-packages-with-npm(npm 包管理)、basic-node-and-express(Node 与 Express 基础)、mongodb-and-mongoose(MongoDB 与 Mongoose)、back-end-development-and-apis-projects(综合项目)。URL Shortener 属于最后一个 block,即学完前三块知识后的综合实战。 - Block 结构文件 curriculum/structure/blocks/back-end-development-and-apis-projects.json 中
challengeOrder定义了五个项目的学习顺序:Timestamp Microservice → Request Header Parser Microservice → URL Shortener Microservice(id 为bd7158d8c443edefaeb5bd0e)→ Exercise Tracker → File Metadata Microservice。 - 认证元数据 curriculum/challenges/english/certifications/back-end-development-and-apis.yml 同样将该 id 列入认证测试清单,与上述结构文件一一对应。
挑战文件本身 curriculum/challenges/english/blocks/back-end-development-and-apis-projects/bd7158d8c443edefaeb5bd0e.md 的 frontmatter 中 challengeType: 4,在 freeCodeCamp 中代表“项目类挑战”——你需要部署一个可公开访问的服务,官方测试会直接对你的线上地址发起 HTTP 请求来验证功能。
二、项目目标与两种开工方式
挑战描述要求:构建一个功能上类似于官方示例(URL Shortener Microservice)的全栈 JavaScript 应用。官方给出了两种标准做法:
- 克隆官方样板仓库完成项目:freeCodeCamp 为该项目提供独立的 boilerplate 仓库
boilerplate-project-urlshortener,包含预置好的项目骨架(Express 应用、测试脚本、package.json 等),将其克隆到本地后在此基础上下手。 - 使用任意托管平台(site builder)完成项目:可以部署在免费云平台,但要求把 boilerplate 仓库中的所有文件都纳入项目——这是硬性要求,因为官方测试代码就是依赖这些文件运行的。
无论哪种方式,交付物都是一个公网可访问的 Node.js 服务。
官方给出的两条关键提示(HINT)
挑战文档的 instructions 部分明确提示:
- 不要忘记使用 body 解析中间件来处理 POST 请求:接收表单数据的 Express 应用必须挂接
bodyParser(或 Express 内置的express.json/express.urlencoded),否则req.body为空,POST 接口无法拿到用户提交的 URL。 - 可以使用 Node.js 核心模块
dns中的dns.lookup(host, cb)来校验提交的 URL:这是判断一个主机名是否真实可解析的标准做法——短链接服务在生成短码前,先对目标地址做 DNS 查询,解析失败即视为非法 URL。
三、API 契约:三个接口的完整行为规范
官方测试基于三条行为定义,任何实现都必须满足。
3.1 POST /api/shorturl:提交长 URL,换取短码
请求方式:application/x-www-form-urlencoded 表单提交,字段为 url。响应必须是一个包含 original_url 与 short_url 两个属性的 JSON 对象,官方示例响应为:
{ "original_url": "https://freeCodeCamp.org", "short_url": 1 }
测试代码(直接摘自挑战文档,可原样运行于 Node 环境):
const url = code; // 你部署的服务地址
const urlVariable = Date.now();
const fullUrl = `${url}/?v=${urlVariable}`;
const res = await fetch(url + '/api/shorturl', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `url=${fullUrl}`
});
if (res.ok) {
const { short_url, original_url } = await res.json();
assert.isNotNull(short_url);
assert.strictEqual(original_url, `${url}/?v=${urlVariable}`);
} else {
throw new Error(`${res.status} ${res.statusText}`);
}
两个断言点:short_url 必须非空(数值或字符串短码均可),original_url 必须原样等于你提交的全量 URL——注意测试提交的 URL 带上了 ?v=${Date.now()} 查询串,因此你的服务必须保留完整 query string 做回显,不能只截取 origin。
3.2 GET /api/shorturl/<short_url>:访问短链接被重定向回原始地址
拿到 short_url 后,GET /api/shorturl/{short_url} 必须把客户端重定向到原始 URL。测试用 fetch 的 redirect 选项做了双重验证:
const url = code;
const urlVariable = Date.now();
const fullUrl = `${url}/?v=${urlVariable}`;
let shortenedUrlVariable;
const postResponse = await fetch(url + '/api/shorturl', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `url=${fullUrl}`
});
if (postResponse.ok) {
const { short_url } = await postResponse.json();
shortenedUrlVariable = short_url;
} else {
throw new Error(`${postResponse.status} ${postResponse.statusText}`);
}
// 自动跟随重定向,确认最终到达新 URL
const getResponse = await fetch(
url + '/api/shorturl/' + shortenedUrlVariable, {redirect:'follow'}
);
if (getResponse) {
const { url } = getResponse; // status is always 200 for some reason
assert.strictEqual(url, fullUrl);
} else {
throw new Error(`${getResponse.status} ${getResponse.statusText}`);
}
// 关闭自动跟随,确认服务器端确实在做重定向
const getManualResponse = await fetch(
url + '/api/shorturl/' + shortenedUrlVariable, {redirect:'manual'}
);
if (getManualResponse) {
const { status } = getManualResponse; // if a redirect happens, it won't reach the new resource
assert.strictEqual(status, 0);
} else {
throw new Error(`${getManualResponse.status} ${getManualResponse.statusText}`);
}
这段测试值得逐行理解,它实际上验证了两个层面:
redirect: 'follow':fetch自动跟随 301/302 跳转,最终落在fullUrl上——证明你的短链接“真的能跳转过去”;redirect: 'manual':不跟随跳转时,Response.status为0(opaqueredirect响应),证明重定向发生在你的服务器侧,而不是你直接返回了一个 200 页面或客户端 JS 跳转。如果你的实现是返回 200 + HTML<meta refresh>或前端路由跳转,这个断言必然失败——必须用 HTTP 层重定向(res.redirect(url)/res.status(301).redirect(url))。
3.3 非法 URL 必须返回 { error: 'invalid url' }
当提交的地址不符合 http://www.example.com 这样的合法格式时,POST 接口应返回包含 error 字段的 JSON:
const url = code;
const res = await fetch(url + '/api/shorturl', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `url=ftp:/john-doe.invalidTLD`
});
if (res.ok) {
const { error } = await res.json();
assert.isNotNull(error);
assert.strictEqual(error.toLowerCase(), 'invalid url');
} else {
throw new Error(`${res.status} ${res.statusText}`);
}
测试用例提交的是 ftp:/john-doe.invalidTLD:非 http(s) 协议、且域名不存在。断言只要求 error 字段存在且内容(忽略大小写)等于 'invalid url',因此错误信息可以直接写死为该字符串。
3.4 提交物约束:不能是官方示例地址
挑战文档的第一条 hint 明确约束:
assert(
!/.*\/url-shortener-microservice\.freecodecamp\.rocks/.test(code)
);
即你提交的服务地址(code)不得匹配官方示例部署域名——必须是你自己部署的项目,直接提交官方示例 URL 会被判不通过。
四、源码级实现要点:从契约到完整代码
结合上述契约与官方提示,一个能跑通全部测试的最小 Express 实现如下(基于 boilerplate 仓库的常见骨架结构):
const express = require('express');
const { DNSResultCode, dnsPromises } = require('dns');
const app = express();
// 提示 1:body 解析中间件,缺失则 POST 拿不到数据
app.use(express.urlencoded({ extended: false }));
app.use(express.json());
// 内存存储;boilerplate 默认骨架即为此形式,生产可换 MongoDB/Mongoose
const db = new Map();
let counter = 0;
app.post('/api/shorturl', async (req, res) => {
const { url } = req.body;
if (!url) return res.json({ error: 'invalid url' });
let parsed;
try {
parsed = new URL(url);
} catch {
return res.json({ error: 'invalid url' });
}
if (!['http:', 'https:'].includes(parsed.protocol)) {
return res.json({ error: 'invalid url' });
}
// 提示 2:dns.lookup 校验主机真实可解析(文档建议的写法是回调版
// dns.lookup(host, cb),这里展示其 Promise 等价形式 dnsPromises.lookup)
try {
await dnsPromises.lookup(parsed.hostname);
} catch (err) {
if (err.code === DNSResultCode.NODATA) {
return res.json({ error: 'invalid url' });
}
throw err;
}
const short_url = ++counter;
db.set(short_url, url);
res.json({ original_url: url, short_url });
});
app.get('/api/shorturl/:short', (req, res) => {
const target = db.get(Number(req.params.short));
if (!target) return res.json({ error: 'invalid url' });
res.redirect(target); // 302 重定向,满足 redirect:'manual' 时 status 为 0 的断言
});
app.listen(process.env.PORT || 3000);
几个实现细节与测试断言直接对应:
original_url原样回显:new URL(url).toString()会规范化(补斜杠、重排 query 等),导致 3.1 节中assert.strictEqual(original_url, fullUrl)失败,所以存取都应使用用户提交的原始字符串,URL解析仅用于校验。dns.lookup的行为边界:它对不存在的域名(如john-doe.invalidTLD)会返回ENOTFOUND/NODATA错误,这正是把“主机不存在”归类为invalid url的依据。挑战文档建议的dns.lookup(host, cb)是回调风格;核心模块dns同时提供同步版lookupSync与 Promise 版dnsPromises.lookup,语义一致。- 重定向用
res.redirect()而非 200 响应,这是 3.2 节redirect: 'manual'断言(status === 0)通过的前提。
五、课程侧的配套机制:测试如何跑起来
理解挑战在平台上的运行方式有助于排查问题。从课程仓库结构看:
- 挑战文件 frontmatter 中的
forumTopicId: 301509指向官方论坛的对应版块,遇到“我的部署为什么没通过测试”类问题可在该版块检索既有讨论; - 该 block 的
blockLayout为project-list(见 curriculum/structure/blocks/back-end-development-and-apis-projects.json),即五个项目以清单形式呈现,各自独立提交、独立判分; - 文档中的测试代码以
code(你提交的服务地址)为入参,fetch+assert的组合说明判分逻辑就是标准的 Node.js HTTP 客户端断言脚本,运行在官方测试环境而非你的服务器上——因此服务必须保持公网可达且长期在线,测试是异步发起的。
六、小结与检查清单
交付前对照以下清单逐项确认,即可覆盖挑战文档中的全部行为约束:
| 检查项 | 对应契约 |
|---|---|
挂接 express.urlencoded(或等效)body 解析中间件 |
POST 能读取 url 字段 |
POST /api/shorturl 返回 { original_url, short_url },original_url 与提交串完全一致 |
测试 1 |
GET /api/shorturl/<short_url> 使用 HTTP 301/302 重定向 |
测试 2(follow 到达原地址;manual 时 status 为 0) |
非法 URL(非 http(s) 或域名不可解析)返回 { error: 'invalid url' } |
测试 3 |
| 提交的是自己的部署地址,而非官方示例域名 | hint 1 正则断言 |
| 项目文件基于 boilerplate 仓库完整构建 | 挑战描述的两条开工方式约束 |
URL Shortener Microservice 体量不大,但它串起了后端认证前三块的核心能力:Express 路由与中间件(body 解析)、核心模块(dns)、状态存储(内存 Map 起步、Mongoose 进阶)与 HTTP 语义(重定向)。按上文契约逐项实现并自测,即可顺利拿下该认证项目。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00