Tại sao .NET Core cho REST API?

.NET Core (nay là .NET 8/9) là một trong những framework hiệu năng cao nhất để build REST API. Benchmark của TechEmpower cho thấy ASP.NET Core liên tục nằm trong top framework nhanh nhất thế giới. Cộng thêm hệ sinh thái phong phú, typing mạnh với C#, và tooling xuất sắc — đây là lựa chọn solid cho backend.

Project Structure

Mình thường tổ chức project theo kiến trúc Clean Architecture đơn giản hóa:

text
MyApi/
├── Controllers/          # Nhận request, trả response
│   └── PostsController.cs
├── Services/             # Business logic
│   ├── IPostService.cs
│   └── PostService.cs
├── Repositories/         # Data access layer
│   ├── IPostRepository.cs
│   └── PostRepository.cs
├── Models/
│   ├── Entities/         # Database entities
│   │   └── Post.cs
│   └── DTOs/             # Data Transfer Objects
│       ├── CreatePostDto.cs
│       └── PostResponseDto.cs
├── Data/
│   └── AppDbContext.cs   # EF Core DbContext
└── Program.cs

Setup Entity Framework Core

c#
// Models/Entities/Post.cs
public class Post
{
    public Guid Id { get; set; } = Guid.NewGuid();
    public string Title { get; set; } = string.Empty;
    public string Slug { get; set; } = string.Empty;
    public string Content { get; set; } = string.Empty;
    public bool IsPublished { get; set; } = false;
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

// Data/AppDbContext.cs
public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Post> Posts => Set<Post>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Post>()
            .HasIndex(p => p.Slug)
            .IsUnique();
    }
}

Controller chuẩn với Response Wrapping

c#
[ApiController]
[Route("api/[controller]")]
public class PostsController : ControllerBase
{
    private readonly IPostService _postService;

    public PostsController(IPostService postService)
    {
        _postService = postService;
    }

    [HttpGet]
    public async Task<IActionResult> GetAll([FromQuery] int page = 1, [FromQuery] int limit = 10)
    {
        var result = await _postService.GetAllAsync(page, limit);
        return Ok(new { data = result.Items, total = result.Total, page, limit });
    }

    [HttpGet("{slug}")]
    public async Task<IActionResult> GetBySlug(string slug)
    {
        var post = await _postService.GetBySlugAsync(slug);
        if (post is null) return NotFound(new { message = "Post not found" });
        return Ok(post);
    }

    [HttpPost]
    [Authorize]
    public async Task<IActionResult> Create([FromBody] CreatePostDto dto)
    {
        var post = await _postService.CreateAsync(dto);
        return CreatedAtAction(nameof(GetBySlug), new { slug = post.Slug }, post);
    }
}

Validation với FluentValidation

c#
public class CreatePostDtoValidator : AbstractValidator<CreatePostDto>
{
    public CreatePostDtoValidator()
    {
        RuleFor(x => x.Title)
            .NotEmpty().WithMessage("Title is required")
            .MaximumLength(200).WithMessage("Title must not exceed 200 characters");

        RuleFor(x => x.Content)
            .NotEmpty().WithMessage("Content is required");

        RuleFor(x => x.Slug)
            .NotEmpty()
            .Matches(@"^[a-z0-9-]+$").WithMessage("Slug must be lowercase letters, numbers and hyphens only");
    }
}

Global Exception Handling

Thay vì try-catch ở mỗi controller, dùng middleware tập trung:

c#
app.UseExceptionHandler(appBuilder => appBuilder.Run(async context =>
{
    context.Response.ContentType = "application/json";
    var exception = context.Features.Get<IExceptionHandlerFeature>()?.Error;

    var (statusCode, message) = exception switch
    {
        NotFoundException ex => (404, ex.Message),
        ValidationException ex => (400, ex.Message),
        UnauthorizedAccessException => (401, "Unauthorized"),
        _ => (500, "An unexpected error occurred")
    };

    context.Response.StatusCode = statusCode;
    await context.Response.WriteAsJsonAsync(new { error = message });
}));

Kết luận

Một REST API tốt không chỉ là "hoạt động được" — nó cần có structure rõ ràng, validation chặt, error messages nhất quán, và dễ mở rộng. .NET Core cung cấp đầy đủ tooling để làm điều này, và C# với static typing giúp bạn bắt lỗi từ compile time thay vì runtime.