Files
pos-system/microservices/.agent/skills/api-design/references/REFERENCE.md
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

16 KiB

API Design - Detailed Reference

This reference contains detailed code examples for RESTful API design patterns in ASP.NET Core.

Standard Response Format / Định Dạng Response Chuẩn

ApiResponse Wrapper

/// <summary>
/// EN: Standard API response wrapper.
/// VI: Wrapper response API chuẩn.
/// </summary>
public class ApiResponse<T>
{
    public bool Success { get; set; }
    public T? Data { get; set; }
    public string? Error { get; set; }
    public PaginationInfo? Pagination { get; set; }
    public MetadataInfo? Metadata { get; set; }
}

public record PaginationInfo(
    int Page,
    int Limit,
    int Total,
    int TotalPages);

public record MetadataInfo(
    string Timestamp,
    string Version,
    string RequestId);

// EN: Success response example / VI: Ví dụ response thành công
// {
//   "success": true,
//   "data": { "id": "123", "email": "user@example.com" },
//   "pagination": { "page": 1, "limit": 10, "total": 100, "totalPages": 10 }
// }

// EN: Error response example / VI: Ví dụ response lỗi
// {
//   "success": false,
//   "error": "User not found"
// }

DTOs (Data Transfer Objects)

Request DTOs with Records

/// <summary>
/// EN: DTO for creating a user.
/// VI: DTO để tạo user.
/// </summary>
public record CreateUserRequest(
    [Required]
    [EmailAddress]
    string Email,
    
    [Required]
    [MinLength(6)]
    string Password,
    
    string? Name);

/// <summary>
/// EN: DTO for updating a user.
/// VI: DTO để cập nhật user.
/// </summary>
public record UpdateUserRequest(
    [EmailAddress]
    string? Email,
    
    string? Name,
    
    string? Avatar);

/// <summary>
/// EN: Query parameters for listing users.
/// VI: Query parameters để list users.
/// </summary>
public record GetUsersQuery(
    int Skip = 0,
    int Take = 20,
    string? Search = null,
    string SortBy = "CreatedAt",
    string Order = "desc");

Response DTOs

/// <summary>
/// EN: DTO for user information.
/// VI: DTO cho thông tin user.
/// </summary>
public record UserDto(
    Guid Id,
    string Email,
    string? Name,
    string? Avatar,
    string Role,
    DateTime CreatedAt,
    DateTime UpdatedAt);

/// <summary>
/// EN: Result for paginated user list.
/// VI: Kết quả danh sách user có phân trang.
/// </summary>
public record UsersListResult(
    IReadOnlyList<UserDto> Users,
    int TotalCount);

/// <summary>
/// EN: Mapper from domain entities to DTOs.
/// VI: Mapper từ domain entities sang DTOs.
/// </summary>
public static class UserDtoMapper
{
    public static UserDto ToDto(this User user) => new(
        user.Id,
        user.Email,
        user.Name,
        user.Avatar,
        user.Role.ToString(),
        user.CreatedAt,
        user.UpdatedAt);
}

Controller Implementation / Triển Khai Controller

Complete Controller Example

/// <summary>
/// EN: Controller for user management.
/// VI: Controller quản lý user.
/// </summary>
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/users")]
[SwaggerTag("User Management - Create, read, update, and delete users")]
public class UsersController : ControllerBase
{
    private readonly IMediator _mediator;
    private readonly ILogger<UsersController> _logger;

    public UsersController(IMediator mediator, ILogger<UsersController> logger)
    {
        _mediator = mediator;
        _logger = logger;
    }

    /// <summary>
    /// EN: Get list of users with pagination.
    /// VI: Lấy danh sách users với phân trang.
    /// </summary>
    [HttpGet]
    [Authorize(Roles = "Admin")]
    [SwaggerOperation(Summary = "List users", Description = "Get paginated list of users")]
    [SwaggerResponse(200, "Users retrieved successfully")]
    [SwaggerResponse(401, "Unauthorized")]
    [SwaggerResponse(403, "Forbidden - Admin role required")]
    public async Task<ActionResult<ApiResponse<UsersListResult>>> GetUsers(
        [FromQuery] int skip = 0,
        [FromQuery] int take = 20,
        [FromQuery] string? search = null,
        CancellationToken cancellationToken = default)
    {
        var query = new GetUsersQuery(skip, take, search);
        var result = await _mediator.Send(query, cancellationToken);

        return Ok(new ApiResponse<UsersListResult>
        {
            Success = true,
            Data = result,
            Pagination = new PaginationInfo(
                Page: skip / take + 1,
                Limit: take,
                Total: result.TotalCount,
                TotalPages: (int)Math.Ceiling(result.TotalCount / (double)take))
        });
    }

    /// <summary>
    /// EN: Get user by ID.
    /// VI: Lấy user theo ID.
    /// </summary>
    [HttpGet("{userId:guid}")]
    [Authorize]
    [SwaggerOperation(Summary = "Get user by ID")]
    [SwaggerResponse(200, "User retrieved successfully")]
    [SwaggerResponse(404, "User not found")]
    public async Task<ActionResult<ApiResponse<UserDto>>> GetUser(
        Guid userId,
        CancellationToken cancellationToken = default)
    {
        var query = new GetUserByIdQuery(userId);
        var result = await _mediator.Send(query, cancellationToken);

        if (result == null)
            return NotFound(new ApiResponse<UserDto>
            {
                Success = false,
                Error = "User not found"
            });

        return Ok(new ApiResponse<UserDto> { Success = true, Data = result });
    }

    /// <summary>
    /// EN: Create a new user.
    /// VI: Tạo user mới.
    /// </summary>
    [HttpPost]
    [SwaggerOperation(Summary = "Create user")]
    [SwaggerResponse(201, "User created successfully")]
    [SwaggerResponse(400, "Invalid request")]
    [SwaggerResponse(409, "User already exists")]
    public async Task<ActionResult<ApiResponse<UserDto>>> CreateUser(
        [FromBody] CreateUserRequest request,
        CancellationToken cancellationToken = default)
    {
        var command = new CreateUserCommand(request.Email, request.Password, request.Name);
        var result = await _mediator.Send(command, cancellationToken);

        if (!result.Success)
            return BadRequest(new ApiResponse<UserDto>
            {
                Success = false,
                Error = result.Error
            });

        return CreatedAtAction(
            nameof(GetUser),
            new { userId = result.User!.Id },
            new ApiResponse<UserDto> { Success = true, Data = result.User });
    }

    /// <summary>
    /// EN: Update user by ID.
    /// VI: Cập nhật user theo ID.
    /// </summary>
    [HttpPut("{userId:guid}")]
    [Authorize]
    [SwaggerOperation(Summary = "Update user")]
    [SwaggerResponse(200, "User updated successfully")]
    [SwaggerResponse(400, "Invalid request")]
    [SwaggerResponse(404, "User not found")]
    public async Task<ActionResult<ApiResponse<UserDto>>> UpdateUser(
        Guid userId,
        [FromBody] UpdateUserRequest request,
        CancellationToken cancellationToken = default)
    {
        // EN: Check if user is updating their own profile or is admin
        // VI: Kiểm tra user có đang cập nhật profile của mình hoặc là admin
        var currentUserId = GetUserId();
        if (currentUserId != userId.ToString() && !User.IsInRole("Admin"))
            return Forbid();

        var command = new UpdateUserCommand(userId, request.Email, request.Name, request.Avatar);
        var result = await _mediator.Send(command, cancellationToken);

        if (!result.Success)
            return NotFound(new ApiResponse<UserDto>
            {
                Success = false,
                Error = result.Error
            });

        return Ok(new ApiResponse<UserDto> { Success = true, Data = result.User });
    }

    /// <summary>
    /// EN: Delete user by ID.
    /// VI: Xóa user theo ID.
    /// </summary>
    [HttpDelete("{userId:guid}")]
    [Authorize(Roles = "Admin")]
    [SwaggerOperation(Summary = "Delete user")]
    [SwaggerResponse(204, "User deleted successfully")]
    [SwaggerResponse(404, "User not found")]
    public async Task<ActionResult> DeleteUser(
        Guid userId,
        CancellationToken cancellationToken = default)
    {
        var command = new DeleteUserCommand(userId);
        var result = await _mediator.Send(command, cancellationToken);

        if (!result.Success)
            return NotFound(new ApiResponse<object>
            {
                Success = false,
                Error = result.Error
            });

        return NoContent();
    }

    private string? GetUserId() =>
        User.FindFirstValue(ClaimTypes.NameIdentifier);
}

MediatR Commands & Queries / Commands & Queries MediatR

Commands

/// <summary>
/// EN: Command to create a user.
/// VI: Command tạo user.
/// </summary>
public record CreateUserCommand(
    string Email,
    string Password,
    string? Name) : IRequest<CreateUserResult>;

public record CreateUserResult(
    bool Success,
    UserDto? User,
    string? Error);

/// <summary>
/// EN: Handler for CreateUserCommand.
/// VI: Handler cho CreateUserCommand.
/// </summary>
public class CreateUserCommandHandler : IRequestHandler<CreateUserCommand, CreateUserResult>
{
    private readonly IUserRepository _userRepository;
    private readonly IPasswordHasher _passwordHasher;

    public CreateUserCommandHandler(
        IUserRepository userRepository,
        IPasswordHasher passwordHasher)
    {
        _userRepository = userRepository;
        _passwordHasher = passwordHasher;
    }

    public async Task<CreateUserResult> Handle(
        CreateUserCommand request,
        CancellationToken cancellationToken)
    {
        // EN: Check if user already exists
        // VI: Kiểm tra user đã tồn tại chưa
        var existingUser = await _userRepository.FindByEmailAsync(request.Email, cancellationToken);
        if (existingUser != null)
            return new CreateUserResult(false, null, "User with this email already exists");

        // EN: Create new user
        // VI: Tạo user mới
        var user = new User(
            email: request.Email,
            passwordHash: _passwordHasher.Hash(request.Password),
            name: request.Name);

        await _userRepository.AddAsync(user, cancellationToken);
        await _userRepository.SaveChangesAsync(cancellationToken);

        return new CreateUserResult(true, user.ToDto(), null);
    }
}

Queries

/// <summary>
/// EN: Query to get users with pagination.
/// VI: Query lấy users với phân trang.
/// </summary>
public record GetUsersQuery(
    int Skip = 0,
    int Take = 20,
    string? Search = null) : IRequest<UsersListResult>;

/// <summary>
/// EN: Handler for GetUsersQuery.
/// VI: Handler cho GetUsersQuery.
/// </summary>
public class GetUsersQueryHandler : IRequestHandler<GetUsersQuery, UsersListResult>
{
    private readonly IUserRepository _userRepository;

    public GetUsersQueryHandler(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public async Task<UsersListResult> Handle(
        GetUsersQuery request,
        CancellationToken cancellationToken)
    {
        var (users, totalCount) = await _userRepository.GetUsersAsync(
            request.Skip,
            request.Take,
            request.Search,
            cancellationToken);

        return new UsersListResult(
            users.Select(u => u.ToDto()).ToList(),
            totalCount);
    }
}

Validation with FluentValidation / Validation với FluentValidation

/// <summary>
/// EN: Validator for CreateUserRequest.
/// VI: Validator cho CreateUserRequest.
/// </summary>
public class CreateUserRequestValidator : AbstractValidator<CreateUserRequest>
{
    public CreateUserRequestValidator()
    {
        RuleFor(x => x.Email)
            .NotEmpty().WithMessage("Email is required")
            .EmailAddress().WithMessage("Invalid email format");

        RuleFor(x => x.Password)
            .NotEmpty().WithMessage("Password is required")
            .MinimumLength(6).WithMessage("Password must be at least 6 characters")
            .Matches("[A-Z]").WithMessage("Password must contain at least one uppercase letter")
            .Matches("[0-9]").WithMessage("Password must contain at least one digit");

        RuleFor(x => x.Name)
            .MaximumLength(100).WithMessage("Name cannot exceed 100 characters");
    }
}

Error Handling / Xử Lý Lỗi

Global Exception Handler Middleware

/// <summary>
/// EN: Global exception handler middleware.
/// VI: Middleware xử lý exception toàn cục.
/// </summary>
public class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ExceptionHandlerMiddleware> _logger;

    public ExceptionHandlerMiddleware(
        RequestDelegate next,
        ILogger<ExceptionHandlerMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        _logger.LogError(exception, "Unhandled exception occurred");

        var (statusCode, error) = exception switch
        {
            ValidationException ve => (400, ve.Message),
            UnauthorizedAccessException => (401, "Unauthorized"),
            KeyNotFoundException => (404, "Resource not found"),
            InvalidOperationException ioe => (409, ioe.Message),
            _ => (500, "An unexpected error occurred")
        };

        context.Response.StatusCode = statusCode;
        context.Response.ContentType = "application/json";

        var response = new ApiResponse<object>
        {
            Success = false,
            Error = error
        };

        await context.Response.WriteAsJsonAsync(response);
    }
}

// EN: Register in Program.cs / VI: Đăng ký trong Program.cs
app.UseMiddleware<ExceptionHandlerMiddleware>();

OpenAPI/Swagger Configuration / Cấu Hình OpenAPI/Swagger

// EN: Configure Swagger in Program.cs
// VI: Cấu hình Swagger trong Program.cs
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo
    {
        Title = "User Service API",
        Version = "v1",
        Description = "User management endpoints for GoodGo platform"
    });

    // EN: Enable XML comments / VI: Bật XML comments
    var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
    var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
    options.IncludeXmlComments(xmlPath);

    // EN: Add JWT authentication / VI: Thêm JWT authentication
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Description = "JWT Authorization header using the Bearer scheme",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
        Scheme = "Bearer"
    });

    options.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type = ReferenceType.SecurityScheme,
                    Id = "Bearer"
                }
            },
            Array.Empty<string>()
        }
    });

    // EN: Enable annotations / VI: Bật annotations
    options.EnableAnnotations();
});

API Versioning Configuration / Cấu Hình API Versioning

// EN: Configure API versioning in Program.cs
// VI: Cấu hình API versioning trong Program.cs
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = new UrlSegmentApiVersionReader();
})
.AddApiExplorer(options =>
{
    options.GroupNameFormat = "'v'VVV";
    options.SubstituteApiVersionInUrl = true;
});

Resources / Tài Nguyên