CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
HTTP 协议与缓存/课时/REST 与资源设计
RawBlame
课时目录12 节

HTTP 协议与缓存

  1. 01HTTP 是什么
  2. 02方法与语义
  3. 03状态码
  4. 04请求与响应头
  5. 05HTTPS 与 TLS
  6. 06HTTP 缓存
  7. 07Cookie 与会话
  8. 08CORS
  9. 09REST 与资源设计
  10. 10HTTP/2 与 HTTP/3
  11. 11应用层缓存对照
  12. 12综合练习

REST 与资源设计

资源 URL、本站 /tracks API 实例。

30 分钟

REST 与资源设计

这一课你将能做到

  1. 用「名词路径 + HTTP 方法 + 状态码」三件套设计一组 tracks/lessons API,而不是 /getTrackList 式动词路径
  2. 分清路径参数(标识资源身份)与查询参数(过滤/分页/瘦身),并说明公开读 /api/** 与管理写 /admin/** 的职责分离
  3. 用 curl 拉列表、详情、lite 变体,亲手写出一张「方法 + 路径 + 成功码 + 失败码」的迷你 API 合同

为什么要学

API 是前后端的合同。路径乱七八糟、动词塞 URL、错误一律 200,客户端、网关、监控全都痛苦:缓存不知道能不能存、前端拦截器没法统一分支、新人接手要靠口口相传。

REST(Representational State Transfer)在工程实践里落地为一套可抄的骨架:资源用 URL 命名,动作用 HTTP 方法表达,结果用状态码表达。面试里「如何设计一个 XX 的 API」考的就是这套骨架运用。本课用课表 API 当样板,不空谈学位论文式 REST 纯度。

先建立图像

资源:tracks 集合、单个 track、其下 lessons
操作:GET 读 / POST 建 / PUT 换 / DELETE 删
结果:200/201/204/404/401…

一张可直接抄进接口文档的设计表:

意图方法路径成功码失败码
课表列表GET/api/tracks200—
课表详情GET/api/tracks/{slug}200404
某课的课节列表GET/api/tracks/{slug}/lessons200404
创建课表(管理)POST/admin/tracks201400/401/403
全量更新PUT/admin/tracks/{id}200400/404
局部更新PATCH/admin/tracks/{id}200400/404
删除DELETE/admin/tracks/{id}204401/403/404
页面(给人看):  /tracks/http-protocol-essentials      → HTML(前端服务)
API(给程序):   /api/tracks/http-protocol-essentials  → JSON(Spring)

结构相似、职责不同:页面可缓存可跳转,API 契约稳定可演进。

跟着做

步骤 1:写一个「资源感」正确的 Controller

@RestController
public class TrackController {

    // 集合:GET /api/tracks
    // 查询参数做过滤,不改资源身份
    @GetMapping("/api/tracks")
    public List<Map<String, Object>> list(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size,
            @RequestParam(required = false) String q) {
        // page/size/q 是「这次查询的条件」,不是「哪个资源」
        return List.of(Map.of("slug", "http-protocol-essentials", "title", "HTTP 必修"));
    }

    // 成员:路径变量标识单个资源
    @GetMapping("/api/tracks/{slug}")
    public ResponseEntity<Map<String, Object>> detail(@PathVariable String slug) {
        if (!"http-protocol-essentials".equals(slug)) {
            return ResponseEntity.notFound().build();   // 找不到 → 404
        }
        return ResponseEntity.ok(Map.of(
            "slug", slug,
            "title", "HTTP 必修",
            "lessonCount", 12));
    }

    // 子资源:资源归属用路径层级表达
    @GetMapping("/api/tracks/{slug}/lessons")
    public List<Map<String, Object>> lessons(@PathVariable String slug) { return List.of(); }
}

步骤 2:curl 验证集合与成员

# 集合(注意引号包裹,防 & 被 shell 吃掉)
curl -s "http://localhost:8080/api/tracks?page=0&size=20&q=http"

# 成员
curl -s http://localhost:8080/api/tracks/http-protocol-essentials

# 不存在 → 404
curl -i http://localhost:8080/api/tracks/no-such-slug

在笔记里画出资源树:

/api/tracks
 └─ /{slug}                ← 路径参数:资源身份
     └─ /lessons           ← 子资源:归属关系

步骤 3:lite 变体——同一资源的多重表示

列表卡片不需要详情的全部字段,给查询参数加一个 lite:

@GetMapping("/api/tracks/{slug}/lessons")
public List<Map<String, Object>> lessons(
        @PathVariable String slug,
        @RequestParam(defaultValue = "false") boolean lite) {
    if (lite) {
        return List.of(Map.of("title", "HTTP 是什么", "order", 1));   // 瘦身版
    }
    return List.of(Map.of("title", "HTTP 是什么", "order", 1,
        "contentMd", "# 很长的正文…", "prereq", List.of("http-intro"))); // 完整版
}
curl -s "http://localhost:8080/api/tracks/http-protocol-essentials/lessons?lite=true"

这体现 REST 的 R——Representational:同一资源可以有多种表示,由参数/Accept 协商,而不是为每种场景造一个新 URL。

步骤 4:管理写接口与鉴权前缀分离

@RestController
@RequestMapping("/admin/tracks")
public class TrackAdminController {

    @PostMapping
    public ResponseEntity<Map<String, Object>> create(@RequestBody TrackForm form) {
        // ... 校验 + 落库(MySQL)
        return ResponseEntity.created(URI.create("/api/tracks/" + form.getSlug()))
                .body(Map.of("slug", form.getSlug()));   // 201 + Location + 新资源表示
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        // ... 逻辑删除
        return ResponseEntity.noContent().build();       // 204
    }
}
/api/**    公开读为主(可缓存、可匿名)
/admin/**  写操作 + 鉴权(Spring Security 按前缀配规则)

权限必须在服务端强制——前端藏按钮只是体验,不是安全。

步骤 5:搜索怎么设计?

搜索本质是「对集合的查询」,优先查询参数:

GET /api/tracks?q=nginx&status=published

两个例外可接受 POST:条件体巨大(超 URL 长度限制)、涉及复杂嵌套结构。此时定义动作资源并文档标明:

POST /api/tracks/search   body: {"keywords":[...],"filters":{...}}

注意 POST 搜索放弃缓存,能 GET 就 GET。

步骤 6:Network 里对照页面与 API 的分工

  1. 打开课程页 → Network → 找 document 请求(返回 text/html)
  2. 再找 api/... 的 XHR/fetch(返回 application/json)
  3. 问自己两个问题:页面路由和 API 路径为什么长得像?数据为什么是 JS 再拉而不是直接嵌进 HTML?

(答案:SSR 页面会把数据预取嵌进 HTML——所以有些页面你看不到独立 API 请求;CSR 页面才明显「先壳后数据」。)

步骤 7:写一份迷你 API 合同(练习)

为「学习进度」假想资源在笔记里写下这张表(不必实现):

意图方法路径成功失败
读我的进度GET/api/tracks/{slug}/progress200401
标记完成PUT/api/tracks/{slug}/progress200400/401
清除进度DELETE/api/tracks/{slug}/progress204401

自检三问:路径是名词层级吗?写操作是否落在需鉴权前缀?状态码是否与状态码课的合同一致?

讲清楚要点

  1. 因为 URL 标识资源,所以用复数名词 tracks,把动作交给方法——/getTrackList、/deleteTrack 让方法语义失效,缓存与监控无从下手。
  2. 因为集合与成员层级清晰,所以嵌套不宜超过两三层——再深(/tracks/{a}/lessons/{b}/notes/{c})改用查询参数或独立资源,URL 一长维护成本飙升。
  3. 因为状态码是合同的一部分,所以创建 201(带 Location)、删除 204、找不到 404、校验失败 400——客户端才能写统一拦截器,监控才能按码告警。
  4. 因为查询参数不改「是哪个资源」的身份,所以过滤/排序/分页/瘦身放 query——page、size、q、lite=true;身份标识(id/slug)放 path。
  5. 因为页面与 API 的消费者不同,所以 /tracks(HTML)与 /api/tracks(JSON)分离——同域由 Nginx 拼接,互不干扰又能共享鉴权与缓存策略。
  6. 因为 REST 是风格不是宗教,所以不必为 HATEOAS 折腾——「资源化命名 + 正确方法 + 正确状态码」贡献了九成收益;超媒体链接留给确有需要的场景。

你可能会懵的地方

  1. 必须严格 REST 才能上线吗? 不必原教旨。内部 RPC、GraphQL 都有生存空间。但对公网 HTTP API,「资源化命名 + 方法语义 + 状态码合同」收益最高、学习成本最低,团队一致性比理论纯度重要。

  2. 对外标识用 slug 还是数字 id? slug 可读、可 SEO(/api/tracks/http-protocol-essentials),但要处理唯一性与改名;数字 id 稳定不泄漏业务含义。常见折中:对外 URL 用 slug,库内主键 id 不暴露;选一种当对外主键并保持稳定。

  3. PUT 必须全量字段吗? 语义上是「整体替换」:没传的字段视为「清空/默认值」。如果只想让客户端提交部分字段却用了 PUT,团队会逐渐混乱——要么真全量,要么 PATCH,要么文档写清「忽略未传字段」(此时其实是 UPSERT 语义)。

  4. 版本号放路径还是头? /api/v1/tracks 简单直白、日志好查,主流选择;Accept: application/vnd.xxx.v2+json 更纯粹但调试麻烦。公开 API 可先不带版本,出现破坏性变更再冻结 v1 加 v2。

  5. 分页参数用 page/size 还是 cursor? 小数据量、需要跳页 → page/size + total;大数据流、无限滚动 → cursor(?after=xxx)避免深分页拖垮 MySQL(LIMIT 100000,20 的问题)。两者都可放进查询参数,不违背资源设计。

错法对照

错法问题正解
/api/getTrackList + 一律 GET动作塞路径,方法语义与缓存全废GET /api/tracks,动作用方法表达
/api/tracks/delete/1 + GET 删除GET 会被预取/爬虫触发,可能误删DELETE /api/tracks/1
业务失败仍 200 + {success:false}拦截器/监控/缓存全部失明HTTP 状态码表达大类,body 详述细节
过滤条件拼成一长串路径 /tracks/published/httpURL 失去资源语义,组合爆炸?status=published&q=http 查询参数
管理写接口与公开读混在同一无鉴权路径权限逻辑散落每个方法,漏一处即事故/admin/** 前缀 + 网关/Security 统一强制鉴权

卡点急救

  1. 前端说「404」,后端说「接口在」:八成是路径拼接不一致或 context-path 前缀。让双方各自 curl 一遍完整 URL 对比;检查 server.servlet.context-path 与网关路由前缀是否叠了两层。
  2. 同一个资源详情,列表页和详情页字段对不上:列表用了 lite 版、详情是全量版——这不是 Bug 是表示差异。把「列表返回哪些字段」写进 API 合同,前端按文档消费,别两个页面互抄字段名。
  3. POST 创建成功后前端要再发一次 GET 拿详情:浪费一次往返。POST 返回 201 + Location + 新资源 body,前端直接用响应体渲染——「创建接口只回 id」是没有必要的保守。

面试常问

  • 如何为一个「文章管理」设计 RESTful API? 给出三件套表:GET /api/articles(列表,?page&q 过滤)、GET /api/articles/{id}(详情)、POST /api/articles(创建,201+Location)、PUT/PATCH /api/articles/{id}(更新)、DELETE /api/articles/{id}(204)。补充:管理操作走 /admin 鉴权前缀;错误按 400/401/403/404 分族;能 GET 不 POST 以保住缓存。

  • RESTful API 的核心原则? 资源化(URL 是名词、层级表归属)、统一接口(HTTP 方法语义)、无状态(每个请求自包含凭证)、分层系统(客户端不必知道后面是缓存还是源站)、可缓存(GET 可缓存)。回答时结合「状态码合同 + 查询参数 vs 路径参数」落地,比背六条学术原则更有说服力。

  • 接口设计里,PUT 和 PATCH 的幂等性有什么讲究? PUT 语义是整体替换,天然幂等,重试安全;PATCH 取决于补丁格式——JSON Merge Patch 幂等,但「自增/追加」类补丁不幂等。涉及计数、追加日志的场景要慎用 PATCH 重试,或引入幂等键。

自测三问

  1. 写出 tracks 资源的列表、详情、创建、删除四条「方法 + 路径 + 成功码」。
  2. 路径参数与查询参数如何分工?各举一个反例说明「放错了会怎样」。
  3. 页面路由 /tracks/... 与 API /api/tracks/... 为什么要分开,却又可以同域共存?

小结一句话

REST 落地三件套:资源用 URL、操作用方法、结果用状态码——公开读与 /admin 写分离,路径参数定身份、查询参数定条件。

延伸

  • MDN:HTTP 概述(资源与 URL 语境)
  • Microsoft REST API Guidelines(工业级设计参考,选读)
  • 下一课:HTTP/2 与 HTTP/3——语义学完了,传输层在怎么进化:多路复用、二进制分帧与 QUIC。
加载本节练习…
← 上一课CORS下一课 →HTTP/2 与 HTTP/3