Day 4 | API 设计 & 全栈串联:前端和后端终于"对话"了


API 设计的核心原则(前端开发者的视角)

前端写 API 调用,天然理解 HTTP 请求。但"设计" API 是什么感觉?

想象你是后端,别人(前端)要来拿数据。你要考虑:

  1. 我要暴露哪些端点? —— 粒度怎么切?
  2. 请求参数怎么传? —— path variable?query string?还是 body JSON?
  3. 返回什么格式? —— 直接返回数据库记录?还是加工过的结构?
  4. 谁来调用? —— 只有你的前端?还是第三方?

这就是 API 设计。


RESTful API 设计入门

RESTful 是一套约定俗成的 API 设计规范,前端开发者其实天天在用它:

操作 HTTP 方法 URL 设计 说明
查全部 GET /api/accidents 获取事故列表
查单个 GET /api/accidents/{id} 获取详情
新增 POST /api/accidents 创建事故
修改 PUT /api/accidents/{id} 更新事故
删除 DELETE /api/accidents/{id} 删除事故
统计 GET /api/accidents/stats 聚合数据(不属于 CRUD,加个 stats)

前端类比: router.get('/accidents') 相当于后端 @GetMapping("/accidents")。RESTful 只是把 URL 当成资源路径来组织,跟 Vue Router 的理念一脉相承。


统一响应结构:前后端对话的"共同语言"

前端 axios 收到响应后,最怕的是格式不统一:

// 有的接口这样返回
{ data: { id: 1, title: '...' } }

// 有的接口这样返回
{ code: 200, message: 'success', data: { id: 1, title: '...' } }

// 有的接口返回 200 但业务失败
{ code: 401, message: '未登录', data: null }

全栈项目必须有统一的响应结构:

// 后端:统一 Result 包装
public class Result<T> {
    private int code;       // 业务状态码(200成功,401未登录,500错误)
    private String message; // 提示信息
    private T data;         // 泛型数据体

    public static <T> Result<T> success(T data) {
        Result<T> r = new Result<>();
        r.code = 200;
        r.message = "success";
        r.data = data;
        return r;
    }

    public static <T> Result<T> fail(int code, String message) {
        Result<T> r = new Result<>();
        r.code = code;
        r.message = message;
        return r;
    }
}
// 前端:axios 响应拦截器统一处理
axios.interceptors.response.use(
  response => {
    const res = response.data
    if (res.code !== 200) {
      // 业务级错误(非 HTTP 401)
      if (res.code === 401) {
        router.push('/login')
      }
      return Promise.reject(res)
    }
    return res.data  // 返回 data 字段给调用方
  },
  error => {
    ElMessage.error(error.response?.data?.message || '网络错误')
    return Promise.reject(error)
  }
)

关键点: 后端返回 HTTP 200 + 业务码 code=401 ≠ HTTP 401。前端 axios 拦截器必须识别业务码 401,跳转登录。这是全栈联调里最容易踩的坑之一。


跨域(CORS)与代理

前端开发时,前端(localhost:5173)调用后端(localhost:8080),浏览器会阻止——这就是 CORS 问题。

开发环境:用 Vite 代理(推荐)

// vite.config.js
export default defineConfig({
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true
      }
    }
  }
})

这样前端 axios.get('/api/accidents') 实际发到 http://localhost:8080/api/accidents,对浏览器来说没有跨域。

生产环境:Nginx 反向代理,把 /api 统一代理到后端。


身份认证:Token 的全栈闭环

前后端分离项目中,身份认证通常是 JWT Token 方案:

登录流程:
前端 → POST /api/login { username, password }
后端 → 验证成功 → 生成 JWT → 返回 { token, user }
前端 → 存 token 到 localStorage,每次请求附上

请求流程:
前端 → GET /api/accidents(header: Authorization: Bearer <token>)
后端 → Filter 拦截 → 解析 JWT → 验证通过 → 放行 Controller

后端 JWT 验证 Filter(简化版):

@Component
public class JwtAuthFilter extends OncePerRequestFilter {
    @Autowired private JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain chain) throws ServletException, IOException {
        // 放行登录接口
        if (request.getRequestURI().contains("/api/login")) {
            chain.doFilter(request, response);
            return;
        }

        String token = request.getHeader("Authorization");
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7);
            if (jwtUtil.validateToken(token)) {
                String username = jwtUtil.getUsernameFromToken(token);
                // 把用户信息存到请求上下文,供 Controller 使用
                request.setAttribute("username", username);
            }
        }
        chain.doFilter(request, response);
    }
}

文件上传:MinIO 对象存储

事故分析系统有图片上传需求。直接存数据库太慢,存服务器本地磁盘不可靠——正确的方案是对象存储。

架构:

前端 → POST /api/upload → 后端 Controller
后端 → 读取文件 → 上传到 MinIO(对象存储服务)
后端 → 返回 MinIO URL → 前端保存到 image_urls 字段
// 后端上传接口(简化版)
@PostMapping("/upload")
public Result<String> upload(@RequestParam("file") MultipartFile file) throws Exception {
    String objectName = UUID.randomUUID() + "-" + file.getOriginalFilename();
    minioClient.putObject(
        PutObjectArgs.builder()
            .bucket("accident-media")
            .object(objectName)
            .stream(file.getInputStream(), file.getSize(), -1)
            .contentType(file.getContentType())
            .build()
    );
    String url = minioClient.getObjectUrl("accident-media", objectName);
    return Result.success(url);
}

前端对应: axios.post('/api/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } })


今日任务清单

  1. 实现完整的登录流程:后端 JWT 登录接口 + 前端 axios 调用 + token 存储 + 请求拦截器附加 token
  2. 配置 Vite 代理:让前端开发服务器正确代理到 localhost:8080,验证 CORS 问题消失
  3. 实现一个事故图片上传接口:后端 MinIO 上传 + 前端 FormData 上传 + 返回 URL 存入 image_urls JSON 字段
  4. AI 挑战:让 AI 给你写一个完整的"事故统计接口"——按严重程度分组统计数量,并让它解释 SQL 怎么写的

思维升级

API 是前后端的"合同"。合同要清晰、统一、版本可控。
全栈开发者的核心竞争力之一,就是能设计出前端用起来舒服、后端维护起来不痛苦的数据接口。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐