别再全是 200 了!—— 一套前后端都能无痛落地的 REST 接口规范

AI摘要
本文提出一套企业级API规范:使用真实HTTP状态码(如404)替代全200响应,结合Java8 Optional处理空值,统一R包装体返回结构。前端通过axios拦截器自动处理业务异常,实现监控、缓存和代码简洁性的全面提升。

关键词:REST、HTTP 状态码、Optional、Null 安全、R 包装体、公司级规范
面向读者:后端、前端、测试、运维
目标:让「用户不存在」真正返回 404,让监控、缓存、Axios 拦截器一起省心


1. 为什么要“小题大做”?

现象 后果
所有接口 HTTP 状态都是 200 CDN 把 404 当成功缓存;前端必须写 if (res.data.code !== 200)
返回 null / 空对象 {} 前端无法区分“没查到”还是“字段全空”,页面白屏
多层 if (obj != null) 代码臃肿,NPE 依旧横行

结论:我们需要

  1. 真 HTTP 状态码(4xx/5xx)
  2. Null 安全(Java8 Optional)
  3. 统一的 R 包装体(兼顾人和机器可读)

2. Java8 Optional —— 只当“返回值”,不当字段/参数

2.1 基本 API 速查表

// 创建
Optional.of(obj)               // 非 null
Optional.ofNullable(obj)       // 可 null
Optional.empty()               // 空容器

// 消费
.isPresent()                   // boolean
.ifPresent(System.out::println)// 存在才消费
.orElse(other)                 // 兜底值
.orElseGet(Supplier)           // 延迟兜底
.orElseThrow(() -> new XxxException("xxx", 404))

// 链式
.map(User::getName)
.filter(s -> s.startsWith("Bei"))
.flatMap(this::findCityByName) // 避免 Optional<Optional<City>>

2.2 典型 DAO → Controller 链路

@GetMapping("/api/v1/users/{id}")
public R<UserVo> getUser(@PathVariable Long id) {
    return userMapper.selectById(id)          // User
        .map(UserConverter::toVo)             // UserVo
        .map(R::data)                         // R<UserVo>
        .orElse(R.fail(404, "用户不存在"));    // 404 + body
}

一行代码完成 null 判断 + 转换 + 真 404;前端 axios.get 直接进 then / catch


3. R 包装体设计(公司级统一)

@Data
@AllArgsConstructor
public final class R<T> implements Serializable {
    private int    code;   // 业务码,0=success,其它=错误
    private String msg;    // 人类可读
    private T      data;   //  payload

    // 快速工厂
    public static <T> R<T> data(T data) {
        return new R<>(0, "success", data);
    }
    public static <T> R<T> fail(int code, String msg) {
        return new R<>(code, msg, null);
    }
}

说明

  • code=0 与 HTTP 200 搭配,表示“业务成功”
  • code≠0 可以与 4xx/5xx 搭配,仅作提示语,不被网关识别
  • 绝不出现 data:null 列表场景返回 data:[],避免前端 data.length 报错

4. 真 · HTTP 状态码使用矩阵

业务含义 HTTP 状态 R.code R.msg 示例 前端 axios 处理
查单条存在 200 0 success response.data.data
查单条不存在 404 404 用户不存在 进 catch,弹 msg
参数校验失败 400 400 手机号格式错误 进 catch,弹 msg
未登录/Token 失效 401 401 请先登录 跳登录页
没权限 403 403 无权限 弹 Forbidden
服务器未知异常 500 500 系统繁忙 统一弹“服务异常”

代码落地

@ResponseStatus(HttpStatus.NOT_FOUND)   // 关键!
public class BizException extends RuntimeException {
    private final int code;
    public BizException(int code, String msg) {
        super(msg);
        this.code = code;
    }
}

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(BizException.class)
    public ResponseEntity<R<Void>> handle(BizException e) {
        R<Void> body = R.fail(e.getCode(), e.getMessage());
        return ResponseEntity.status(e.getCode()).body(body);
    }
}

5. 列表/分页 & 空数据返回约定

场景 HTTP R.data 前端断言
列表/分页 200 []{list:[],total:0} list.length === 0 显示“暂无数据”
Map/树 200 {} Object.keys(data).length
布尔/计数 200 false / 0 直接取值

禁止返回 data:null;前端不再写 data && data.map 防御式代码


6. 日志 & 监控对接

  • Nginx/AccessLog:只看 HTTP 状态 → 4xx/5xx 即错误
  • Prometheus:相同规则,告警规则无需改造
  • ELK/SLSstatus>=400 即异常采样,减少噪点
  • Swagger:文档里能正确出现 200/404/400 多个 Response Example,Mock 更真实

7. 前端参考模板(axios 统一拦截)

import axios from 'axios';
import { Message } from 'element-ui';
import router from '@/router';

const http = axios.create({ baseURL: '/api' });

http.interceptors.response.use(
  res => {
    // 只有 HTTP 200 会进这里
    const body = res.data;
    if (body.code === 0) return body.data;   // 直接返回业务数据
    Message.error(body.msg);                // 业务失败,弹提示
    return Promise.reject(body);
  },
  err => {
    // 所有 HTTP 4xx/5xx 进这里
    const status = err.response?.status;
    const msg = err.response?.data?.msg || '网络异常';
    if (status === 401) router.push('/login');
    else Message.error(msg);
    return Promise.reject(err);
  }
);

export default http;

前端再也不用写 if (res.data.code !== 200),真正的“关注业务,不关注 plumbing”


8. 常见坑 & QA

Q1. 404 会被公司网关重定向到友好页面?
→ 让运维把 /api/** 设为“纯接口域名”或关闭错误页重定向即可

Q2. 老项目全 200,改不动?
→ 新接口遵循即可;老接口加 @Deprecated 逐步切换,监控里把 code!=0 当错误先跑着

Q3. Optional 做字段/参数行不行?
→ 不行。序列化(Jackson)、Swagger 示例、可读性都会崩;只当返回值

Q4. 列表接口真的需要 404 吗?
→ 不需要。空列表是合法业务结果,HTTP 200 + data:[] 即可


9. 一句话总结(背下来)

“HTTP 状态码先认错,Optional 消灭 null,R 包装体给人看,监控缓存都省心。”


10. 附录:完整示例工程(SpringBoot + MyBatis)

# pom 关键依赖
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>3.0.1</version>
</dependency>

分享完毕,欢迎 CR/提 Issue,一起把“200 全家桶”扔进历史!

本作品采用《CC 协议》,转载必须注明作者和本文链接
MissYou-Coding
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
Coding Peasant @ 互联网
文章
194
粉丝
10
喜欢
60
收藏
66
排名:598
访问:1.3 万
私信
所有博文
博客标签
社区赞助商