Skip to content

09 · 与后端框架集成:Spring / Express / NestJS / FastAPI

生活类比: 写 yaml 是"先画菜单再开餐厅"——design-first; 在代码里写注解、自动导出 yaml 是"先开餐厅再贴菜单"——code-first。 两条路都能到罗马,绝大多数后端团队走的是 code-first(写代码顺手出文档)。


1. 两种工作流:Design-first vs Code-first

Design-first              Code-first
────────────              ──────────
1. 写 openapi.yaml         1. 写代码 + 加注解
2. 评审 / 改                 2. 启动服务,自动暴露 /v3/api-docs
3. 后端 implements          3. yaml 由框架自动生成
4. 前端用 codegen 出 SDK     4. 前端拉 yaml 出 SDK

✅ 大型项目 / 团队           ✅ 个人 / 中小项目
✅ 多语言后端协作            ✅ 改代码不用同步改 yaml
❌ 早期开发慢                ❌ yaml 容易"脏",得 lint

推荐:项目早期 design-first 锁定接口;日常迭代 code-first 提效。


2. Spring Boot · springdoc-openapi(推荐)

注:早年用的 springfox 已经不维护了,新项目用 springdoc-openapi

2.1 加依赖

xml
<!-- pom.xml -->
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.5.0</version>
</dependency>

启动后 零配置

  • yaml: http://localhost:8080/v3/api-docs.yaml
  • json: http://localhost:8080/v3/api-docs
  • UI: http://localhost:8080/swagger-ui.html

2.2 全局配置

java
@Configuration
public class OpenApiConfig {
    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
            .info(new Info()
                .title("电商 API")
                .version("1.0.0")
                .description("基于 Spring Boot 3 + springdoc-openapi"))
            .components(new Components()
                .addSecuritySchemes("bearerAuth",
                    new SecurityScheme()
                        .type(SecurityScheme.Type.HTTP)
                        .scheme("bearer")
                        .bearerFormat("JWT")))
            .addSecurityItem(new SecurityRequirement().addList("bearerAuth"));
    }
}

2.3 Controller 注解

java
@RestController
@RequestMapping("/users")
@Tag(name = "用户管理")
public class UserController {

    @GetMapping
    @Operation(summary = "获取用户列表", operationId = "listUsers")
    @ApiResponses({
        @ApiResponse(responseCode = "200", description = "成功"),
        @ApiResponse(responseCode = "401", description = "未登录")
    })
    public List<User> list(
        @Parameter(description = "页码") @RequestParam(defaultValue = "1") int page) {
        return userService.list(page);
    }

    @PostMapping
    @Operation(summary = "创建用户", operationId = "createUser",
               security = @SecurityRequirement(name = "bearerAuth"))
    public User create(@Valid @RequestBody UserCreate body) {
        return userService.create(body);
    }
}

2.4 DTO 注解

java
@Schema(description = "创建用户请求")
public class UserCreate {

    @Schema(description = "用户名", example = "alice", required = true)
    @NotBlank
    @Size(max = 50)
    private String name;

    @Schema(description = "邮箱", example = "alice@x.com", required = true)
    @Email
    private String email;
}

Spring @Valid 配合 JSR-303 注解(@NotBlank@Size@Email...)会自动反映在 OpenAPI schema 里——少写一份


3. Express + swagger-jsdoc · 手写注释模式

3.1 安装

bash
npm i swagger-jsdoc swagger-ui-express

3.2 启动入口

js
// app.js
const express = require('express');
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');

const app = express();

const spec = swaggerJsdoc({
  definition: {
    openapi: '3.0.3',
    info: { title: 'Express API', version: '1.0.0' },
    servers: [{ url: 'http://localhost:3000' }]
  },
  apis: ['./routes/*.js']     // 从这些文件里提取 JSDoc 注释
});

app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(spec));
app.listen(3000);

3.3 路由文件用 JSDoc 注释

js
// routes/users.js
const router = require('express').Router();

/**
 * @openapi
 * /users:
 *   get:
 *     summary: 获取用户列表
 *     tags: [user]
 *     parameters:
 *       - in: query
 *         name: page
 *         schema: { type: integer, default: 1 }
 *     responses:
 *       200:
 *         description: 用户数组
 *         content:
 *           application/json:
 *             schema:
 *               type: array
 *               items: { $ref: '#/components/schemas/User' }
 */
router.get('/', (req, res) => res.json(userService.list()));

/**
 * @openapi
 * components:
 *   schemas:
 *     User:
 *       type: object
 *       required: [id, name, email]
 *       properties:
 *         id: { type: integer }
 *         name: { type: string }
 *         email: { type: string, format: email }
 */
module.exports = router;

Express 没有反射,所以只能手写注释。看着繁琐,但胜在零侵入业务代码


4. NestJS · @nestjs/swagger(最优雅)

4.1 安装

bash
npm i @nestjs/swagger

4.2 启动入口

ts
// main.ts
import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder()
    .setTitle('NestJS API')
    .setVersion('1.0')
    .addBearerAuth()
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document);

  await app.listen(3000);
}
bootstrap();

4.3 Controller + DTO

ts
// users.controller.ts
import { Controller, Get, Post, Body, Query, Param } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';

@ApiTags('user')
@Controller('users')
export class UsersController {

  @Get()
  @ApiOperation({ summary: '获取用户列表' })
  @ApiResponse({ status: 200, type: [User] })
  list(@Query('page') page = 1): User[] {
    return [];
  }

  @Post()
  @ApiBearerAuth()
  @ApiOperation({ summary: '创建用户' })
  create(@Body() dto: CreateUserDto): User {
    return this.usersService.create(dto);
  }
}

// dto/create-user.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty } from 'class-validator';

export class CreateUserDto {
  @ApiProperty({ example: 'Alice' })
  @IsNotEmpty()
  name: string;

  @ApiProperty({ example: 'alice@x.com' })
  @IsEmail()
  email: string;
}

4.4 杀手锏:CLI 插件自动推断装饰器

nest-cli.json

json
{
  "compilerOptions": {
    "plugins": [
      {
        "name": "@nestjs/swagger",
        "options": { "introspectComments": true }
      }
    ]
  }
}

效果:不写 @ApiProperty 也能从 TS 类型自动推断 + 从 JSDoc 注释抽 description。懒人福音


5. FastAPI · 内置,零配置(最爽)

5.1 装好 FastAPI 就完事

bash
pip install fastapi uvicorn[standard]
python
# main.py
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr, Field

app = FastAPI(title="FastAPI 示例", version="1.0.0")

class UserCreate(BaseModel):
    name: str = Field(..., max_length=50, example="Alice")
    email: EmailStr = Field(..., example="alice@x.com")

class User(BaseModel):
    id: int
    name: str
    email: EmailStr

@app.get("/users", response_model=list[User], tags=["user"], summary="用户列表")
def list_users(page: int = 1):
    return []

@app.post("/users", response_model=User, status_code=201, tags=["user"], summary="创建用户")
def create_user(user: UserCreate):
    return User(id=1, **user.dict())

启动:

bash
uvicorn main:app --reload

打开:

  • Swagger UI: http://localhost:8000/docs
  • Redoc: http://localhost:8000/redoc
  • yaml: http://localhost:8000/openapi.json

零注解、零配置。FastAPI 用 Pydantic 类型提示 直接生成完整的 OpenAPI 文档——这是其他框架做不到的优雅。


6. 其他常见框架速查

6.1 Go (Gin) + swaggo/swag

go
// @title 用户 API
// @version 1.0
func main() { ... }

// @Summary 获取用户
// @Tags user
// @Param id path int true "用户 ID"
// @Success 200 {object} User
// @Router /users/{id} [get]
func getUser(c *gin.Context) { ... }
bash
swag init                        # 生成 docs/ 目录
# 然后挂载 gin-swagger 中间件即可

6.2 Django REST + drf-spectacular

python
# settings.py
INSTALLED_APPS = [..., 'drf_spectacular']
REST_FRAMEWORK = {
    'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema'
}
SPECTACULAR_SETTINGS = { 'TITLE': 'My API', 'VERSION': '1.0.0' }

# urls.py
from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView
urlpatterns = [
    path('schema/', SpectacularAPIView.as_view(), name='schema'),
    path('docs/', SpectacularSwaggerView.as_view(url_name='schema')),
]

6.3 .NET Core + Swashbuckle

csharp
// Program.cs
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

if (app.Environment.IsDevelopment()) {
    app.UseSwagger();
    app.UseSwaggerUI();
}

6.4 Rust (actix) + utoipa

rust
#[utoipa::path(
    get,
    path = "/users/{id}",
    responses((status = 200, body = User))
)]
async fn get_user(id: web::Path<i32>) -> impl Responder { ... }

7. 对比一张表

框架集成包默认 UI 路径工作方式推荐度
Spring Bootspringdoc-openapi-starter/swagger-ui.html注解⭐⭐⭐⭐⭐
Expressswagger-jsdoc + swagger-ui-express自定义JSDoc 注释⭐⭐⭐
NestJS@nestjs/swagger/api装饰器(CLI 插件可省)⭐⭐⭐⭐⭐
FastAPI内置/docsPydantic 类型⭐⭐⭐⭐⭐
Go (Gin)swaggo/swag + gin-swagger/swagger/index.html注释⭐⭐⭐⭐
Django RESTdrf-spectacular/docs/类自动⭐⭐⭐⭐
ASP.NET CoreSwashbuckle/swagger反射⭐⭐⭐⭐
Rust (actix)utoipa自定义⭐⭐⭐

8. Code-first 的两个常见坑

8.1 注解 vs 真实代码不一致

java
@PostMapping("/users")
@ApiResponse(responseCode = "200")    // ← 写错了!应该是 201
public ResponseEntity<User> create(...) {
    return ResponseEntity.status(201).body(...);
}

解决:CI 里跑接口测试(schemathesis / Newman),用真实响应去校 yaml。

8.2 注解写得满天飞,可读性差

java
@Operation(...) @ApiResponses(...) @Parameter(...)
@SecurityRequirement(...) @Tag(...) @RequestMapping(...)
public User get(...) { /* 业务才 1 行 */ }

解决

  • 简单接口靠类型推断(NestJS CLI 插件、FastAPI Pydantic)
  • 复杂接口注解抽到一个单独的 Doc 类 / interface

9. Hybrid:design-first + code-first 混合

很多大型团队的做法:

1. 用 Stoplight Studio 设计 openapi.yaml  ← design-first
2. 用 openapi-generator 生成 server interface
3. 后端写 implements,业务在实现里
4. CI 检查:导出真实运行时 yaml ↔ 仓库里的 yaml diff,不一致就拒合

这样既享受了 design-first 的"先评审后写代码",又有 code-first 的"代码即真相"。


10. 章末面试题速览

  1. springfox 和 springdoc-openapi 的区别? → springfox 已停止维护,且只支持 OpenAPI 2.0(Swagger 2)。新项目用 springdoc-openapi(支持 OAS 3 + Spring Boot 3)。
  2. NestJS 的 CLI 插件有什么用? → 不写 @ApiProperty 也能从 TS 类型 / JSDoc 自动生成 schema,少写一半注解。
  3. FastAPI 为什么文档"自动"出来? → Python 类型提示 + Pydantic 模型 = 编译期就能拿到完整 schema,框架直接组装出 OpenAPI yaml。

🎬 实战代码

下方放了 4 个框架的最小可运行示例(同一份 User CRUD 接口),你可以照抄到自己项目。

→ 看 09_integration/code/ 目录

💻 示例代码

javascript
// ============================================================
//  Express + swagger-jsdoc + swagger-ui-express 最小可运行示例
//  访问: http://localhost:3000/api-docs
// ============================================================

const express = require('express');
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');

const app = express();
app.use(express.json());

const spec = swaggerJsdoc({
  definition: {
    openapi: '3.0.3',
    info: {
      title: 'Express 用户 API',
      version: '1.0.0',
      description: 'Express + swagger-jsdoc 示例',
    },
    servers: [{ url: 'http://localhost:3000', description: '本地' }],
    components: {
      securitySchemes: {
        bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' },
      },
      schemas: {
        User: {
          type: 'object',
          required: ['id', 'name', 'email'],
          properties: {
            id: { type: 'integer', example: 1 },
            name: { type: 'string', example: 'Alice' },
            email: { type: 'string', format: 'email', example: 'alice@x.com' },
          },
        },
        UserCreate: {
          type: 'object',
          required: ['name', 'email'],
          properties: {
            name: { type: 'string', maxLength: 50 },
            email: { type: 'string', format: 'email' },
          },
        },
        Error: {
          type: 'object',
          properties: {
            code: { type: 'integer' },
            message: { type: 'string' },
          },
        },
      },
    },
    security: [{ bearerAuth: [] }],
  },
  apis: ['./app.js', './routes/*.js'],
});

app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(spec));
app.get('/openapi.json', (req, res) => res.json(spec));

const store = new Map();
let nextId = 1;

/**
 * @openapi
 * /users:
 *   get:
 *     tags: [user]
 *     summary: 用户列表
 *     security: []
 *     parameters:
 *       - in: query
 *         name: page
 *         schema: { type: integer, default: 1 }
 *     responses:
 *       200:
 *         description: 成功
 *         content:
 *           application/json:
 *             schema:
 *               type: array
 *               items: { $ref: '#/components/schemas/User' }
 */
app.get('/users', (req, res) => {
  res.json(Array.from(store.values()));
});

/**
 * @openapi
 * /users/{id}:
 *   get:
 *     tags: [user]
 *     summary: 查单条
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema: { type: integer }
 *     responses:
 *       200:
 *         description: 成功
 *         content:
 *           application/json:
 *             schema: { $ref: '#/components/schemas/User' }
 *       404:
 *         description: 不存在
 *         content:
 *           application/json:
 *             schema: { $ref: '#/components/schemas/Error' }
 */
app.get('/users/:id', (req, res) => {
  const u = store.get(Number(req.params.id));
  if (!u) return res.status(404).json({ code: 404, message: 'not found' });
  res.json(u);
});

/**
 * @openapi
 * /users:
 *   post:
 *     tags: [user]
 *     summary: 创建用户
 *     requestBody:
 *       required: true
 *       content:
 *         application/json:
 *           schema: { $ref: '#/components/schemas/UserCreate' }
 *     responses:
 *       201:
 *         description: 创建成功
 *         content:
 *           application/json:
 *             schema: { $ref: '#/components/schemas/User' }
 */
app.post('/users', (req, res) => {
  const id = nextId++;
  const u = { id, ...req.body };
  store.set(id, u);
  res.status(201).json(u);
});

/**
 * @openapi
 * /users/{id}:
 *   delete:
 *     tags: [user]
 *     summary: 删除用户
 *     parameters:
 *       - in: path
 *         name: id
 *         required: true
 *         schema: { type: integer }
 *     responses:
 *       204:
 *         description: 已删除
 */
app.delete('/users/:id', (req, res) => {
  store.delete(Number(req.params.id));
  res.status(204).send();
});

app.listen(3000, () => {
  console.log('🌐 http://localhost:3000/api-docs');
  console.log('📋 http://localhost:3000/openapi.json');
});
json
{
  "name": "swagger-express-demo",
  "version": "1.0.0",
  "description": "Express + swagger-jsdoc 示例",
  "main": "app.js",
  "scripts": {
    "start": "node app.js",
    "dev": "nodemon app.js"
  },
  "dependencies": {
    "express": "^4.19.2",
    "swagger-jsdoc": "^6.2.8",
    "swagger-ui-express": "^5.0.0"
  },
  "devDependencies": {
    "nodemon": "^3.1.0"
  }
}
python
# ============================================================
#  FastAPI 最小可运行示例(零额外配置!)
#  启动: uvicorn main:app --reload
#  访问: http://localhost:8000/docs (Swagger UI)
#         http://localhost:8000/redoc (Redoc)
#         http://localhost:8000/openapi.json
# ============================================================

from typing import List, Optional
from fastapi import FastAPI, HTTPException, Path, Query, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel, EmailStr, Field

app = FastAPI(
    title="FastAPI 用户 API",
    version="1.0.0",
    description="FastAPI 内置 Swagger UI 示例 —— 几乎零额外注解",
    contact={"name": "张三", "email": "dev@example.com"},
    servers=[
        {"url": "http://localhost:8000", "description": "本地"},
        {"url": "https://api.example.com", "description": "生产"},
    ],
)

bearer = HTTPBearer(auto_error=False)


class UserBase(BaseModel):
    name: str = Field(..., max_length=50, examples=["Alice"])
    email: EmailStr = Field(..., examples=["alice@x.com"])


class User(UserBase):
    id: int = Field(..., examples=[1])


class UserCreate(UserBase):
    pass


store: dict[int, User] = {}
next_id = 1


@app.get("/users", response_model=List[User], tags=["user"], summary="用户列表")
def list_users(
    page: int = Query(1, ge=1, description="页码(从 1 开始)"),
    size: int = Query(20, ge=1, le=100, description="每页大小"),
):
    items = list(store.values())
    return items[(page - 1) * size : page * size]


@app.get(
    "/users/{user_id}",
    response_model=User,
    tags=["user"],
    summary="查单条",
    responses={404: {"description": "用户不存在"}},
)
def get_user(user_id: int = Path(..., description="用户 ID", ge=1)):
    u = store.get(user_id)
    if not u:
        raise HTTPException(404, "用户不存在")
    return u


@app.post(
    "/users",
    response_model=User,
    status_code=201,
    tags=["user"],
    summary="创建用户",
    dependencies=[Depends(bearer)],
)
def create_user(body: UserCreate):
    global next_id
    u = User(id=next_id, **body.dict())
    store[next_id] = u
    next_id += 1
    return u


@app.delete(
    "/users/{user_id}",
    status_code=204,
    tags=["user"],
    summary="删除用户",
    dependencies=[Depends(bearer)],
)
def delete_user(user_id: int):
    store.pop(user_id, None)
txt
fastapi==0.115.0
uvicorn[standard]==0.30.6
pydantic[email]==2.9.2
typescript
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';

@Module({
  controllers: [UsersController],
})
export class AppModule {}
typescript
// ============================================================
//  NestJS + @nestjs/swagger 最小可运行示例
//  访问: http://localhost:3000/api
// ============================================================

import { NestFactory } from '@nestjs/core';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));

  const config = new DocumentBuilder()
    .setTitle('NestJS 用户 API')
    .setDescription('@nestjs/swagger 完整示例')
    .setVersion('1.0.0')
    .addServer('http://localhost:3000', '本地')
    .addBearerAuth()
    .addTag('user', '用户管理')
    .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('api', app, document, {
    swaggerOptions: { persistAuthorization: true },
  });

  await app.listen(3000);
  console.log('🌐 http://localhost:3000/api');
}
bootstrap();
typescript
import { Controller, Get, Post, Delete, Body, Param, Query, HttpCode, NotFoundException } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsOptional, MaxLength } from 'class-validator';

export class User {
  @ApiProperty({ example: 1 })
  id: number;

  @ApiProperty({ example: 'Alice' })
  name: string;

  @ApiProperty({ example: 'alice@x.com', format: 'email' })
  email: string;
}

export class CreateUserDto {
  @ApiProperty({ example: 'Alice', maxLength: 50 })
  @IsNotEmpty()
  @MaxLength(50)
  name: string;

  @ApiProperty({ example: 'alice@x.com', format: 'email' })
  @IsEmail()
  email: string;
}

@ApiTags('user')
@Controller('users')
export class UsersController {
  private store = new Map<number, User>();
  private nextId = 1;

  @Get()
  @ApiOperation({ summary: '用户列表', operationId: 'listUsers' })
  @ApiResponse({ status: 200, type: [User] })
  list(@Query('page') page = 1, @Query('size') size = 20): User[] {
    const all = Array.from(this.store.values());
    return all.slice((page - 1) * size, page * size);
  }

  @Get(':id')
  @ApiOperation({ summary: '查单条', operationId: 'getUser' })
  @ApiResponse({ status: 200, type: User })
  @ApiResponse({ status: 404, description: '不存在' })
  get(@Param('id') id: number): User {
    const u = this.store.get(Number(id));
    if (!u) throw new NotFoundException();
    return u;
  }

  @Post()
  @ApiBearerAuth()
  @ApiOperation({ summary: '创建用户', operationId: 'createUser' })
  @ApiResponse({ status: 201, type: User })
  create(@Body() dto: CreateUserDto): User {
    const u: User = { id: this.nextId++, ...dto };
    this.store.set(u.id, u);
    return u;
  }

  @Delete(':id')
  @ApiBearerAuth()
  @ApiOperation({ summary: '删除用户', operationId: 'deleteUser' })
  @ApiResponse({ status: 204, description: '已删除' })
  @HttpCode(204)
  delete(@Param('id') id: number): void {
    this.store.delete(Number(id));
  }
}
markdown
# 09 章 · 后端集成示例代码

四个框架,**同一份 User CRUD 接口**,让你横向对比注解 vs 类型推断。

code/ ├── spring-boot/ ← Spring Boot 3 + springdoc-openapi │ ├── pom.xml │ ├── UserController.java │ └── OpenApiConfig.java ├── express/ ← Express + swagger-jsdoc │ ├── package.json │ └── app.js ├── nestjs/ ← NestJS + @nestjs/swagger │ ├── main.ts │ ├── app.module.ts │ └── users.controller.ts └── fastapi/ ← FastAPI(内置!) ├── requirements.txt └── main.py


## 启动

| 框架        | 命令                               | 文档地址                         |
| ----------- | ---------------------------------- | -------------------------------- |
| Spring Boot | `mvn spring-boot:run`              | http://localhost:8080/swagger-ui.html |
| Express     | `npm i && npm start`               | http://localhost:3000/api-docs   |
| NestJS      | `npm i && npx ts-node src/main.ts` | http://localhost:3000/api        |
| FastAPI     | `pip install -r requirements.txt && uvicorn main:app --reload` | http://localhost:8000/docs |

## 几个直观对比

### 创建用户接口的"代码量"

| 框架        | 大致行数(含 DTO + 注解) | 主要靠              |
| ----------- | ------------------------- | ------------------- |
| Spring Boot | ~50 行                    | 注解 (@Operation 等) |
| Express     | ~30 行(含 JSDoc)         | JSDoc 手写注释       |
| NestJS      | ~30 行                    | 装饰器 + 类型        |
| FastAPI     | ~10 行                    | Pydantic 类型        |

> FastAPI 一个 `def create_user(body: UserCreate) -> User` 就能生成完整文档。这就是"动态语言 + 严格类型提示"的力量。
java
package com.example.demo;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.List;

@Configuration
public class OpenApiConfig {

    @Bean
    public OpenAPI api() {
        return new OpenAPI()
            .info(new Info()
                .title("电商示例 API")
                .version("1.0.0")
                .description("Spring Boot 3 + springdoc-openapi 完整示例")
                .contact(new Contact().name("张三").email("dev@example.com"))
                .license(new License().name("MIT")))
            .servers(List.of(
                new Server().url("http://localhost:8080").description("本地"),
                new Server().url("https://api.example.com").description("生产")
            ))
            .components(new Components()
                .addSecuritySchemes("bearerAuth",
                    new SecurityScheme()
                        .type(SecurityScheme.Type.HTTP)
                        .scheme("bearer")
                        .bearerFormat("JWT")))
            .addSecurityItem(new SecurityRequirement().addList("bearerAuth"));
    }
}
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.0</version>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>swagger-springdoc-demo</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

        <!-- 关键依赖:自动暴露 /swagger-ui.html 和 /v3/api-docs -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.5.0</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
java
// ============================================================
//  Spring Boot 3 + springdoc-openapi 最小可运行示例
//  访问: http://localhost:8080/swagger-ui.html
// ============================================================

package com.example.demo;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;

@RestController
@RequestMapping("/users")
@Tag(name = "用户管理", description = "用户的 CRUD")
public class UserController {

    private final Map<Long, User> store = new ConcurrentHashMap<>();
    private final AtomicLong idGen = new AtomicLong(1);

    @GetMapping
    @Operation(summary = "用户列表", operationId = "listUsers")
    public List<User> list(
        @Parameter(description = "页码(从 1 开始)") @RequestParam(defaultValue = "1") int page,
        @Parameter(description = "每页大小") @RequestParam(defaultValue = "20") int size) {
        return store.values().stream()
            .skip((long)(page - 1) * size)
            .limit(size)
            .toList();
    }

    @GetMapping("/{id}")
    @Operation(summary = "查单条", operationId = "getUser")
    @ApiResponses({
        @ApiResponse(responseCode = "200", description = "成功"),
        @ApiResponse(responseCode = "404", description = "用户不存在")
    })
    public ResponseEntity<User> get(@PathVariable Long id) {
        User u = store.get(id);
        return u == null ? ResponseEntity.notFound().build() : ResponseEntity.ok(u);
    }

    @PostMapping
    @Operation(summary = "创建用户", operationId = "createUser",
               security = @SecurityRequirement(name = "bearerAuth"))
    @ApiResponse(responseCode = "201", description = "创建成功")
    public ResponseEntity<User> create(@Valid @RequestBody UserCreate body) {
        long id = idGen.getAndIncrement();
        User u = new User(id, body.name, body.email);
        store.put(id, u);
        return ResponseEntity.status(HttpStatus.CREATED).body(u);
    }

    @DeleteMapping("/{id}")
    @Operation(summary = "删除用户", operationId = "deleteUser",
               security = @SecurityRequirement(name = "bearerAuth"))
    @ApiResponse(responseCode = "204", description = "已删除")
    public ResponseEntity<Void> delete(@PathVariable Long id) {
        return store.remove(id) != null
            ? ResponseEntity.noContent().build()
            : ResponseEntity.notFound().build();
    }

    // ---------------- DTO ----------------

    @Schema(description = "用户")
    public record User(
        @Schema(description = "ID", example = "1") Long id,
        @Schema(description = "姓名", example = "Alice") String name,
        @Schema(description = "邮箱", example = "alice@x.com") String email
    ) {}

    @Schema(description = "创建用户请求体")
    public static class UserCreate {
        @Schema(description = "姓名", example = "Alice", required = true)
        @NotBlank @Size(max = 50)
        public String name;

        @Schema(description = "邮箱", example = "alice@x.com", required = true)
        @Email @NotBlank
        public String email;
    }
}

express/app.js ↗ · express/package.json ↗ · fastapi/main.py ↗ · fastapi/requirements.txt ↗ · nestjs/app.module.ts ↗ · nestjs/main.ts ↗ · nestjs/users.controller.ts ↗ · README.md ↗ · spring-boot/OpenApiConfig.java ↗ · spring-boot/pom.xml ↗ · spring-boot/UserController.java ↗