# 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 ```csharp /// /// EN: Standard API response wrapper. /// VI: Wrapper response API chuẩn. /// public class ApiResponse { 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 ```csharp /// /// EN: DTO for creating a user. /// VI: DTO để tạo user. /// public record CreateUserRequest( [Required] [EmailAddress] string Email, [Required] [MinLength(6)] string Password, string? Name); /// /// EN: DTO for updating a user. /// VI: DTO để cập nhật user. /// public record UpdateUserRequest( [EmailAddress] string? Email, string? Name, string? Avatar); /// /// EN: Query parameters for listing users. /// VI: Query parameters để list users. /// public record GetUsersQuery( int Skip = 0, int Take = 20, string? Search = null, string SortBy = "CreatedAt", string Order = "desc"); ``` ### Response DTOs ```csharp /// /// EN: DTO for user information. /// VI: DTO cho thông tin user. /// public record UserDto( Guid Id, string Email, string? Name, string? Avatar, string Role, DateTime CreatedAt, DateTime UpdatedAt); /// /// EN: Result for paginated user list. /// VI: Kết quả danh sách user có phân trang. /// public record UsersListResult( IReadOnlyList Users, int TotalCount); /// /// EN: Mapper from domain entities to DTOs. /// VI: Mapper từ domain entities sang DTOs. /// 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 ```csharp /// /// EN: Controller for user management. /// VI: Controller quản lý user. /// [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 _logger; public UsersController(IMediator mediator, ILogger logger) { _mediator = mediator; _logger = logger; } /// /// EN: Get list of users with pagination. /// VI: Lấy danh sách users với phân trang. /// [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>> 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 { Success = true, Data = result, Pagination = new PaginationInfo( Page: skip / take + 1, Limit: take, Total: result.TotalCount, TotalPages: (int)Math.Ceiling(result.TotalCount / (double)take)) }); } /// /// EN: Get user by ID. /// VI: Lấy user theo ID. /// [HttpGet("{userId:guid}")] [Authorize] [SwaggerOperation(Summary = "Get user by ID")] [SwaggerResponse(200, "User retrieved successfully")] [SwaggerResponse(404, "User not found")] public async Task>> 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 { Success = false, Error = "User not found" }); return Ok(new ApiResponse { Success = true, Data = result }); } /// /// EN: Create a new user. /// VI: Tạo user mới. /// [HttpPost] [SwaggerOperation(Summary = "Create user")] [SwaggerResponse(201, "User created successfully")] [SwaggerResponse(400, "Invalid request")] [SwaggerResponse(409, "User already exists")] public async Task>> 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 { Success = false, Error = result.Error }); return CreatedAtAction( nameof(GetUser), new { userId = result.User!.Id }, new ApiResponse { Success = true, Data = result.User }); } /// /// EN: Update user by ID. /// VI: Cập nhật user theo ID. /// [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>> 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 { Success = false, Error = result.Error }); return Ok(new ApiResponse { Success = true, Data = result.User }); } /// /// EN: Delete user by ID. /// VI: Xóa user theo ID. /// [HttpDelete("{userId:guid}")] [Authorize(Roles = "Admin")] [SwaggerOperation(Summary = "Delete user")] [SwaggerResponse(204, "User deleted successfully")] [SwaggerResponse(404, "User not found")] public async Task 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 { Success = false, Error = result.Error }); return NoContent(); } private string? GetUserId() => User.FindFirstValue(ClaimTypes.NameIdentifier); } ``` ## MediatR Commands & Queries / Commands & Queries MediatR ### Commands ```csharp /// /// EN: Command to create a user. /// VI: Command tạo user. /// public record CreateUserCommand( string Email, string Password, string? Name) : IRequest; public record CreateUserResult( bool Success, UserDto? User, string? Error); /// /// EN: Handler for CreateUserCommand. /// VI: Handler cho CreateUserCommand. /// public class CreateUserCommandHandler : IRequestHandler { private readonly IUserRepository _userRepository; private readonly IPasswordHasher _passwordHasher; public CreateUserCommandHandler( IUserRepository userRepository, IPasswordHasher passwordHasher) { _userRepository = userRepository; _passwordHasher = passwordHasher; } public async Task 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 ```csharp /// /// EN: Query to get users with pagination. /// VI: Query lấy users với phân trang. /// public record GetUsersQuery( int Skip = 0, int Take = 20, string? Search = null) : IRequest; /// /// EN: Handler for GetUsersQuery. /// VI: Handler cho GetUsersQuery. /// public class GetUsersQueryHandler : IRequestHandler { private readonly IUserRepository _userRepository; public GetUsersQueryHandler(IUserRepository userRepository) { _userRepository = userRepository; } public async Task 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 ```csharp /// /// EN: Validator for CreateUserRequest. /// VI: Validator cho CreateUserRequest. /// public class CreateUserRequestValidator : AbstractValidator { 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 ```csharp /// /// EN: Global exception handler middleware. /// VI: Middleware xử lý exception toàn cục. /// public class ExceptionHandlerMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; public ExceptionHandlerMiddleware( RequestDelegate next, ILogger 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 { Success = false, Error = error }; await context.Response.WriteAsJsonAsync(response); } } // EN: Register in Program.cs / VI: Đăng ký trong Program.cs app.UseMiddleware(); ``` ## OpenAPI/Swagger Configuration / Cấu Hình OpenAPI/Swagger ```csharp // 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() } }); // EN: Enable annotations / VI: Bật annotations options.EnableAnnotations(); }); ``` ## API Versioning Configuration / Cấu Hình API Versioning ```csharp // 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 - [ASP.NET Core Web API Best Practices](https://docs.microsoft.com/en-us/aspnet/core/web-api/) - [MediatR Documentation](https://github.com/jbogard/MediatR) - [FluentValidation Documentation](https://docs.fluentvalidation.net/) - [Swashbuckle Documentation](https://github.com/domaindrivendev/Swashbuckle.AspNetCore)