logo
火山博客
导航

后端接口设计的12个关键实践

2025-02-07
13阅读时间10分钟

在使用 Node.js/Nest.js 进行后端开发时,一个优秀的接口设计对于系统的可用性、可维护性和性能都至关重要。本文将结合具体示例,分享12个后端接口设计的关键实践。

1. 严格的参数校验

参数校验是接口安全性的第一道防线。在 Nest.js 中,我们可以使用 class-validator 优雅地处理参数校验:

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

export class CreateUserDto {
  @ApiProperty({ description: '用户名' })
  @IsNotEmpty({ message: '用户名不能为空' })
  @Length(4, 20)
  username: string;

  @ApiProperty({ description: '邮箱' })
  @IsEmail({}, { message: '邮箱格式不正确' })
  email: string;

  @ApiProperty({ description: '手机号' })
  @Matches(/^1[3-9]\d{9}$/, { message: '手机号格式不正确' })
  phone: string;
}

// user.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { ApiOperation, ApiResponse } from '@nestjs/swagger';

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Post()
  @ApiOperation({ summary: '创建用户' })
  @ApiResponse({ status: 201, description: '用户创建成功' })
  async createUser(@Body() createUserDto: CreateUserDto) {
    return this.userService.create(createUserDto);
  }
}

2. 接口的扩展性设计

设计接口时要遵循"开放封闭原则"。以通知服务为例:

Typescript
// notification.service.ts
interface INotification {
  send(message: string, target: string): Promise<void>;
}

@Injectable()
class EmailNotification implements INotification {
  async send(message: string, email: string) {
    // 发送邮件逻辑
  }
}

@Injectable()
class SMSNotification implements INotification {
  async send(message: string, phone: string) {
    // 发送短信逻辑
  }
}

@Injectable()
export class NotificationService {
  private strategies = new Map<string, INotification>();

  constructor(
    private emailNotification: EmailNotification,
    private smsNotification: SMSNotification,
  ) {
    this.strategies.set('email', emailNotification);
    this.strategies.set('sms', smsNotification);
  }

  async notify(type: string, message: string, target: string) {
    const strategy = this.strategies.get(type);
    if (!strategy) {
      throw new Error('不支持的通知类型');
    }
    return strategy.send(message, target);
  }
}

3. 接口幂等性保证

使用 Redis 实现幂等性控制:

Typescript
// idempotency.middleware.ts
@Injectable()
export class IdempotencyMiddleware implements NestMiddleware {
  constructor(private readonly redis: Redis) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const token = req.headers['idempotency-key'];
    if (!token) {
      throw new BadRequestException('缺少幂等性token');
    }

    const key = `idempotency:${token}`;
    const exists = await this.redis.set(key, '1', 'NX', 'EX', 300);
    
    if (!exists) {
      throw new ConflictException('重复请求');
    }

    next();
  }
}

4. 关键日志打印

使用 Winston 进行日志管理:

Typescript
// logger.service.ts
@Injectable()
export class LoggerService {
  private logger: winston.Logger;

  constructor() {
    this.logger = winston.createLogger({
      level: 'info',
      format: winston.format.combine(
        winston.format.timestamp(),
        winston.format.json()
      ),
      transports: [
        new winston.transports.File({ filename: 'error.log', level: 'error' }),
        new winston.transports.File({ filename: 'combined.log' })
      ]
    });
  }

  logRequest(req: Request, res: Response) {
    this.logger.info({
      type: 'request',
      method: req.method,
      path: req.path,
      params: req.params,
      body: req.body,
      response: res.statusCode
    });
  }
}

5. 核心接口线程池隔离

在 Node.js 中使用 worker_threads 实现线程池:

Typescript
// thread-pool.service.ts
import { Worker } from 'worker_threads';

@Injectable()
export class ThreadPoolService {
  private workers: Worker[] = [];
  private taskQueue: any[] = [];
  
  constructor() {
    // 初始化工作线程池
    for (let i = 0; i < 4; i++) {
      const worker = new Worker('./worker.js');
      this.workers.push(worker);
    }
  }

  async executeTask(task: any) {
    // 任务分配逻辑
  }
}

6. 第三方接口异常处理

调用第三方服务时需要考虑以下几个方面:

  1. 超时控制:设置合理的超时时间,避免请求长时间阻塞
  2. 异常重试:对于偶发性错误进行有限次数的重试
  3. 熔断降级:当服务异常率达到阈值时快速失败
  4. 结果验证:验证返回数据的完整性和正确性

以下是一个结合了这些特性的最佳实践示例:

Typescript
// external-api.service.ts
@Injectable()
export class ExternalApiService {
  constructor(private circuitBreaker: CircuitBreakerService) {}

  @Retry(3, 1000) // 重试装饰器:3次,间隔1秒
  async callExternalAPI(data: any) {
    return await this.circuitBreaker.execute(async () => {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), 5000);

      try {
        const response = await fetch('http://api.example.com', {
          method: 'POST',
          body: JSON.stringify(data),
          signal: controller.signal
        });

        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }

        const result = await response.json();
        // 验证响应数据
        if (!this.validateResponse(result)) {
          throw new Error('Invalid response data');
        }

        return result;
      } finally {
        clearTimeout(timeoutId);
      }
    });
  }
}

7. 异步处理机制

对于非核心流程(如发送通知、日志记录等),应采用异步处理机制:

  1. 消息队列:适用于任务解耦和削峰填谷
  2. 事件驱动:适用于系统内部的异步通知
  3. 定时任务:适用于周期性任务处理
  4. 异步线程池:适用于CPU密集型任务

下面是使用消息队列的最佳实践示例:

Typescript
// notification.service.ts
@Injectable()
export class NotificationService {
  constructor(
    @InjectQueue('notifications') private notificationsQueue: Queue
  ) {}

  async sendNotification(userId: string, message: string) {
    // 核心业务处理完成后,异步发送通知
    await this.notificationsQueue.add('send-notification', {
      userId,
      message
    }, {
      attempts: 3,
      backoff: {
        type: 'exponential',
        delay: 1000
      },
      removeOnComplete: true
    });
  }
}

// notification.processor.ts
@Processor('notifications')
export class NotificationProcessor {
  @Process('send-notification')
  async handleNotification(job: Job) {
    const { userId, message } = job.data;
    
    try {
      // 发送通知的具体实现
      await this.sendNotificationToUser(userId, message);
    } catch (error) {
      // 记录错误日志
      this.logger.error(`Failed to send notification: ${error.message}`);
      throw error; // 抛出错误以触发重试机制
    }
  }
}

8. 并行查询优化

对于复杂查询接口,可以使用 Promise.all 实现并行调用:

Typescript
// user.service.ts
@Injectable()
export class UserService {
  async getUserFullInfo(userId: string) {
    const [userInfo, orderInfo, otherInfo] = await Promise.all([
      this.getUserInfo(userId),
      this.getOrderInfo(userId),
      this.getOtherInfo(userId)
    ]);

    return {
      userInfo,
      orderInfo,
      otherInfo
    };
  }

  // 也可以使用 Promise.allSettled 处理部分失败的情况
  async getUserFullInfoWithFallback(userId: string) {
    const results = await Promise.allSettled([
      this.getUserInfo(userId),
      this.getOrderInfo(userId),
      this.getOtherInfo(userId)
    ]);

    return results.map(result => 
      result.status === 'fulfilled' ? result.value : null
    );
  }
}

9. 合理的锁粒度

在并发场景下,锁的粒度要恰当:

  1. 只对共享资源加锁:避免对非共享资源加锁,减少不必要的性能开销
  2. 缩小锁的范围:尽可能减少锁定的时间
  3. 避免锁的嵌套:防止死锁风险
  4. 选择合适的锁类型:根据场景选择读写锁、互斥锁等
Typescript
// lock.service.ts
import { Injectable } from '@nestjs/common';
import { RedisService } from '@nestjs/redis';

@Injectable()
export class LockService {
  constructor(private readonly redisService: RedisService) {}

  async acquireLock(key: string, timeout: number = 5000): Promise<boolean> {
    const redis = this.redisService.getClient();
    const lockValue = Date.now().toString();
    
    const acquired = await redis.set(
      `lock:${key}`,
      lockValue,
      'NX',
      'PX',
      timeout
    );
    
    return !!acquired;
  }

  async releaseLock(key: string): Promise<void> {
    const redis = this.redisService.getClient();
    await redis.del(`lock:${key}`);
  }
}

10. 避免长事务

长事务会占用系统资源,应该:

  1. 将RPC调用移出事务:避免外部调用影响事务时间
  2. 查询操作放在事务外:读操作不需要事务保护
  3. 使用编程式事务控制范围:精确控制事务边界
  4. 合理设置事务超时:防止事务长时间挂起
Typescript
// transaction.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Connection, Repository } from 'typeorm';

@Injectable()
export class TransactionService {
  constructor(
    private connection: Connection,
    @InjectRepository(User)
    private userRepo: Repository<User>
  ) {}

  async createUserWithProfile(userData: CreateUserDto) {
    // 查询操作放在事务外
    const existingUser = await this.userRepo.findOne({ 
      where: { email: userData.email } 
    });
    
    if (existingUser) {
      throw new ConflictException('用户已存在');
    }

    // 开启事务
    const queryRunner = this.connection.createQueryRunner();
    await queryRunner.connect();
    await queryRunner.startTransaction();

    try {
      const user = await queryRunner.manager.save(User, userData);
      await queryRunner.manager.save(UserProfile, {
        userId: user.id,
        ...userData.profile
      });

      await queryRunner.commitTransaction();
      return user;
    } catch (err) {
      await queryRunner.rollbackTransaction();
      throw err;
    } finally {
      await queryRunner.release();
    }
  }
}

11. 接口限流保护

使用 Redis 实现分布式限流:

Typescript
// rate-limit.guard.ts
@Injectable()
export class RateLimitGuard implements CanActivate {
  constructor(private readonly redis: Redis) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const ip = request.ip;
    const key = `ratelimit:${ip}`;
    
    const [count] = await this.redis
      .multi()
      .incr(key)
      .expire(key, 60)
      .exec();

    if (count > 100) {
      throw new HttpException('请求过于频繁', HttpStatus.TOO_MANY_REQUESTS);
    }

    return true;
  }
}

12. 接口安全防护

实现签名验证中间件:

Typescript
// signature.middleware.ts
@Injectable()
export class SignatureMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const timestamp = req.headers['x-timestamp'];
    const signature = req.headers['x-signature'];
    const body = req.body;

    // 验证时间戳是否过期
    if (Date.now() - Number(timestamp) > 5 * 60 * 1000) {
      throw new UnauthorizedException('请求已过期');
    }

    // 验证签名
    const calculatedSignature = this.calculateSignature(timestamp, body);
    if (signature !== calculatedSignature) {
      throw new UnauthorizedException('签名验证失败');
    }

    next();
  }

  private calculateSignature(timestamp: string, body: any): string {
    // 签名计算逻辑
    return crypto
      .createHash('md5')
      .update(`${timestamp}${JSON.stringify(body)}${SECRET_KEY}`)
      .digest('hex');
  }
}

总结

以上是基于 Node.js/Nest.js 的后端接口设计实践。这些原则和示例代码可以帮助我们构建更加健壮和可维护的后端服务。在实际应用中,需要根据具体业务场景和性能需求,灵活运用这些设计原则。

关键点回顾:

  1. 接口安全性:参数校验、签名验证、限流保护
  2. 性能优化:并行查询、异步处理、合理的锁粒度
  3. 可用性保障:幂等性控制、异常处理、熔断降级
  4. 可维护性:日志记录、接口扩展性、事务管理
2024 © Powered by
hsBlog
|
后台管理