搜索

PythonReactSEOPDF
GitHub Skill

vendix-backend-api

API endpoint patterns.

Rzyfront☆ 4 仓库 Stars0 仓库 Forks

安装与使用

请根据来源仓库和你的 AI 工具说明安装此技能。先阅读技能文件和依赖要求,再添加到工作流。

查看来源 ↗使用指南

技能说明

Vendix Backend API Patterns

API Response Pattern - Respuestas estandarizadas, DTOs y estructura de controladores.

🎯 Standard API Response

ResponseService

File: common/responses/response.service.ts

import { Injectable } from '@nestjs/common';

interface SuccessResponse<T> {
  success: true;
  data: T;
  message?: string;
  meta?: any;
}

interface ErrorResponse {
  success: false;
  error: {
    message: string;
    code?: string;
    details?: any;
  };
}

@Injectable()
export class ResponseService {
  success<T>(data: T, message?: string, meta?: any): SuccessResponse<T> {
    return {
      success: true,
      data,
      message,
      meta,
    };
  }

  error(message: string, code?: string, details?: any): ErrorResponse {
    return {
      success: false,
      error: {
        message,
        code,
        details,
      },
    };
  }

  paginated<T>(data: T[], meta: PaginationMeta) {
    return this.success(data, undefined, { pagination: meta });
  }
}

interface PaginationMeta {
  total: number;
  page: number;
  limit: number;
  total_pages: number;
}

metadata: scope: [root] auto_invoke: "Creating API endpoints"

📦 Controller Pattern

Standard Controller Structure
import { Controller, Get, Post, Put, Delete, Body, Param, Query } from '@nestjs/common';
import { {Domain}Service } from './{domain}.service';
import { ResponseService } from '@/common/responses/response.service';
import { {Action}Dto } from './dto/{action}-dto.dto';
import { Public } from '@/common/decorators/public.decorator';
import { Permissions } from '@/common/decorators/permissions.decorator';

@Controller('domains/:domain_id/{resource}')  // Multi-tenant route
export class {Resource}Controller {
  constructor(
    private readonly {resource}_service: {Resource}Service,
    private readonly response_service: ResponseService,
  ) {}

  @Get()
  async findAll(@Query() query_dto: Query{Resource}Dto) {
    const result = await this.{resource}_service.findAll(query_dto);
    return this.response_service.paginated(result.data, result.meta);
  }

  @Get(':id')
  async findOne(@Param('id') id: string) {
    const result = await this.{resource}_service.findOne(+id);
    return this.response_service.success(result);
  }

  @Post()
  @Permissions('{resource}:write')
  async create(@Body() create_dto: Create{Resource}Dto) {
    const result = await this.{resource}_service.create(create_dto);
    return this.response_service.success(result, 'Created successfully');
  }

  @Put(':id')
  @Permissions('{resource}:write')
  async update(@Param('id') id: string, @Body() update_dto: Update{Resource}Dto) {
    const result = await this.{resource}_service.update(+id, update_dto);
    return this.response_service.success(result, 'Updated successfully');
  }

  @Delete(':id')
  @Permissions('{resource}:delete')
  async remove(@Param('id') id: string) {
    await this.{resource}_service.remove(+id);
    return this.response_service.success(null, 'Deleted successfully');
  }
}

📝 DTO Patterns

Create DTO
import { IsString, IsEmail, IsOptional, MinLength, IsNumber, IsBoolean } from 'class-validator';

export class CreateUserDto {
  @IsString()
  @MinLength(3)
  user_name: string;

  @IsEmail()
  email: string;

  @IsString()
  @MinLength(8)
  password: string;

  @IsOptional()
  @IsString()
  phone_number?: string;

  @IsOptional()
  @IsBoolean()
  is_active?: boolean = true;
}
Update DTO (Partial)
import { PartialType } from '@nestjs/mapped-types';
import { CreateUserDto } from './create-user.dto';

export class UpdateUserDto extends PartialType(CreateUserDto) {}
Query DTO
import { IsOptional, IsString, IsNumber, IsBoolean } from 'class-validator';
import { Type } from 'class-transformer';

export class QueryUserDto {
  @IsOptional()
  @IsString()
  search?: string;

  @IsOptional()
  @Type(() => Number)
  @IsNumber()
  page?: number = 1;

  @IsOptional()
  @Type(() => Number)
  @IsNumber()
  limit?: number = 10;

  @IsOptional()
  @IsString()
  sort_by?: string = 'created_at';

  @IsOptional()
  @IsString()
  sort_order?: 'asc' | 'desc' = 'desc';

  @IsOptional()
  @IsBoolean()
  include_deleted?: boolean = false;
}

🔄 Service Response Pattern

Standard Service Methods
@Injectable()
export class UserService {
  constructor(
    private readonly prisma: EcommercePrismaService,
    private readonly response_service: ResponseService,
  ) {}

  async findAll(query: QueryUserDto) {
    const { page, limit, search, sort_by, sort_order } = query;

    const skip = (page - 1) * limit;

    const where = {
      ...this.prisma.organizationWhere,
      ...(search && {
        OR: [
          { user_name: { contains: search, mode: 'insensitive' } },
          { email: { contains: search, mode: 'insensitive' } },
        ],
      }),
    };

    const [data, total] = await Promise.all([
      this.prisma.users.findMany({
        where,
        skip,
        take: limit,
        orderBy: { [sort_by]: sort_order },
      }),
      this.prisma.users.count({ where }),
    ]);

    return {
      data,
      meta: {
        total,
        page,
        limit,
        total_pages: Math.ceil(total / limit),
      },
    };
  }

  async findOne(id: number) {
    const user = await this.prisma.users.findFirst({
      where: {
        id,
        ...this.prisma.organizationWhere,
      },
    });

    if (!user) {
      throw new NotFoundException('User not found');
    }

    return user;
  }

  async create(create_dto: CreateUserDto) {
    // Check if email exists
    const existing = await this.prisma.users.findUnique({
      where: { email: create_dto.email },
    });

    if (existing) {
      throw new ConflictException('Email already exists');
    }

    // Hash password
    const hashed_password = await bcrypt.hash(create_dto.password, 10);

    // Create user
    const user = await this.prisma.users.create({
      data: {
        ...create_dto,
        password: hashed_password,
        organization_id: this.prisma.context.organization_id,
      },
      select: {
        id: true,
        user_name: true,
        email: true,
        phone_number: true,
        is_active: true,
        created_at: true,
      },  // Exclude password from response
    });

    return user;
  }

  async update(id: number, update_dto: UpdateUserDto) {
    await this.findOne(id);  // Check existence

    if (update_dto.password) {
      update_dto.password = await bcrypt.hash(update_dto.password, 10);
    }

    const user = await this.prisma.users.update({
      where: { id },
      data: update_dto,
      select: {
        id: true,
        user_name: true,
        email: true,
        phone_number: true,
        is_active: true,
        updated_at: true,
      },
    });

    return user;
  }

  async remove(id: number) {
    await this.findOne(id);  // Check existence

    await this.prisma.users.delete({
      where: { id },
    });
  }
}

🌐 HTTP Status Codes

Standard Status Codes
Scenario Status Code Description
Success 200 Request succeeded
Created 201 Resource created
No Content 204 Success, no response body
Bad Request 400 Invalid input
Unauthorized 401 Not authenticated
Forbidden 403 Authenticated but not authorized
Not Found 404 Resource not found
Conflict 409 Resource conflict (duplicate)
Unprocessable Entity 422 Validation error
Internal Server Error 500 Server error
Usage in Controllers
@Post()
@HttpCode(HttpStatus.CREATED)  // 201
async create(@Body() dto: CreateDto) {
  return this.service.create(dto);
}

@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)  // 204
async remove(@Param('id') id: string) {
  await this.service.remove(+id);
}

🎯 Error Handling

Exception Filters

File: common/filters/http-exception.filter.ts

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Response } from 'express';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const status = exception.getStatus();
    const exception_response = exception.getResponse();

    const error_response = {
      success: false,
      error: {
        message: exception.message,
        code: exception.name,
        details: exception_response,
      },
    };

    response.status(status).json(error_response);
  }
}
Global Exception Filter

File: main.ts

import { ValidationPipe } from '@nestjs/common';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';

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

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,  // Strip properties not in DTO
      transform: true,  // Auto-transform types
      forbidNonWhitelisted: true,  // Throw error for non-whitelisted
    }),
  );

  app.useGlobalFilters(new HttpExceptionFilter());

  await app.listen(3000);
}

🔍 Key Files Reference

File Purpose
common/responses/response.service.ts Standardized responses
common/filters/http-exception.filter.ts Global error handling
main.ts Global pipes and filters
dto/*.dto.ts Data transfer objects

Related Skills

  • vendix-backend-domain - Domain architecture
  • vendix-backend-auth - Authentication and authorization
  • vendix-validation - Validation patterns
  • vendix-naming-conventions - Naming conventions (CRITICAL)

使用示例

查看来源仓库中的使用示例与配置说明。

查看来源 ↗

来源历史

收录时间
2026-01-20
索引更新时间
2026-03-03
Branch
main

收录与同步日期属于本站记录。源码修改历史请查看来源仓库。

GitHub ↗

chat Comments (0)

chat_bubble_outline

No comments yet. Be the first to share your thoughts!