# Best Practices khi thiết kế Web API trong .NET

Khi xây dựng Web API, phần khó không nằm ở việc tạo ra endpoint có thể chạy được.

Phần khó là làm sao để API:

– Dễ hiểu.
– Dễ maintain.
– Dễ mở rộng.
– Dễ debug.
– An toàn.
– Có performance tốt.
– Không phá client khi backend thay đổi.

Một API tốt không chỉ là API trả về `200 OK`.

Nó cần có thiết kế nhất quán từ:

“`text
URL
HTTP Method
Status Code
Validation
Authentication
Authorization
Logging
Exception Handling
Pagination
Versioning
Database Access
Observability
“`

Trong bài này chúng ta sẽ đi qua những best practice quan trọng khi xây dựng Web API trong .NET.

—

# 1. Thiết kế URL theo resource, không theo action

Một lỗi phổ biến là thiết kế API như RPC:

“`http
POST /api/create-user
POST /api/update-user
POST /api/delete-user
POST /api/get-user
“`

Cách này vẫn chạy được nhưng không tận dụng đúng semantic của HTTP.

Nên thiết kế theo resource:

“`http
GET /api/users
GET /api/users/{id}
POST /api/users
PUT /api/users/{id}
DELETE /api/users/{id}
“`

Ở đây:

“`text
/users
“`

là resource.

HTTP Method thể hiện action.

—

# 2. Sử dụng HTTP Method đúng mục đích

Một guideline phổ biến:

| Method | Mục đích |
|—|—|
| GET | Lấy dữ liệu |
| POST | Tạo mới hoặc thực hiện command |
| PUT | Replace/update toàn bộ resource |
| PATCH | Update một phần |
| DELETE | Xóa resource |

Ví dụ:

“`http
GET /api/users/123
“`

lấy user.

“`http
POST /api/users
“`

tạo user.

“`http
PATCH /api/users/123
“`

update một số field.

“`http
DELETE /api/users/123
“`

xóa user.

Không nên làm:

“`http
GET /api/delete-user?id=123
“`

vì GET về nguyên tắc không nên thay đổi state của server.

—

# 3. GET nên là safe operation

Một nguyên tắc quan trọng:

> GET không nên gây side effect thay đổi business state.

Ví dụ không nên:

“`http
GET /api/users/123/activate
“`

Nếu endpoint này làm thay đổi trạng thái user.

Nên dùng:

“`http
POST /api/users/123/activate
“`

hoặc tùy business design:

“`http
PATCH /api/users/123
“`

—

# 4. Sử dụng HTTP Status Code đúng

Không nên trả:

“`http
200 OK
“`

cho tất cả mọi trường hợp.

Ví dụ:

## Success

“`text
200 OK
“`

cho request thành công.

“`text
201 Created
“`

khi tạo resource mới.

“`text
204 No Content
“`

khi request thành công nhưng không cần response body.

—

## Client Error

“`text
400 Bad Request
“`

request không hợp lệ.

“`text
401 Unauthorized
“`

chưa authenticated.

“`text
403 Forbidden
“`

đã authenticated nhưng không có permission.

“`text
404 Not Found
“`

resource không tồn tại.

“`text
409 Conflict
“`

business conflict hoặc concurrency conflict.

“`text
422 Unprocessable Entity
“`

có thể dùng cho validation/business semantic errors trong một số thiết kế API.

—

## Server Error

“`text
500 Internal Server Error
“`

server gặp lỗi ngoài dự kiến.

Không nên làm:

“`json
{
“success”: false,
“statusCode”: 200,
“message”: “User not found”
}
“`

nếu thực tế resource không tồn tại.

HTTP status nên phản ánh đúng kết quả request.

—

# 5. Response nên nhất quán

Một API khó maintain thường có response mỗi endpoint một kiểu.

Ví dụ endpoint A:

“`json
{
“data”: {}
}
“`

endpoint B:

“`json
{
“result”: {}
}
“`

endpoint C:

“`json
{
“value”: {}
}
“`

Nên chọn một convention nhất quán.

Ví dụ:

“`json
{
“data”: {
“id”: “123”,
“name”: “Vu”
}
}
“`

Error:

“`json
{
“code”: “USER_NOT_FOUND”,
“message”: “User not found”
}
“`

Với .NET, có thể cân nhắc dùng:

“`text
ProblemDetails
“`

theo chuẩn RFC.

Ví dụ:

“`json
{
“type”: “https://example.com/errors/user-not-found”,
“title”: “User not found”,
“status”: 404,
“detail”: “The requested user does not exist.”
}
“`

—

# 6. Không expose Entity trực tiếp

Một lỗi khá phổ biến trong EF Core:

“`csharp
[HttpGet(“{id}”)]
public async Task Get(Guid id)
{
return await _dbContext.Users.FindAsync(id);
}
“`

Cách này có nhiều risk:

– Expose field không mong muốn.
– Coupling API contract với database schema.
– Circular reference.
– Lazy loading issue.
– Khó version API.
– Client phụ thuộc trực tiếp vào entity.

Nên dùng DTO.

“`csharp
public class UserResponse
{
public Guid Id { get; set; }

public string Name { get; set; }

public string Email { get; set; }
}
“`

Mapping:

“`csharp
return new UserResponse
{
Id = user.Id,
Name = user.Name,
Email = user.Email
};
“`

Một rule quan trọng:

> Database model và API contract không nên là cùng một object.

—

# 7. Tách Request DTO và Response DTO

Không nên dùng cùng một DTO cho mọi mục đích.

Ví dụ:

“`csharp
public class UserDto
{
public Guid Id { get; set; }

public string Name { get; set; }

public string Email { get; set; }

public DateTime CreatedAt { get; set; }
}
“`

rồi dùng object này để create user.

Client có thể gửi:

“`json
{
“id”: “fake-id”,
“name”: “Vu”,
“email”: “test@example.com”,
“createdAt”: “2030-01-01”
}
“`

Nên tách:

“`csharp
public class CreateUserRequest
{
public string Name { get; set; }

public string Email { get; set; }
}
“`

và:

“`csharp
public class UserResponse
{
public Guid Id { get; set; }

public string Name { get; set; }

public string Email { get; set; }

public DateTime CreatedAt { get; set; }
}
“`

—

# 8. Validation phải thực hiện ở boundary

API nên validate input càng sớm càng tốt.

Ví dụ:

“`csharp
public class CreateUserRequest
{
[Required]
public string Name { get; set; }

[Required]
[EmailAddress]
public string Email { get; set; }
}
“`

Hoặc dùng FluentValidation.

Ví dụ:

“`csharp
RuleFor(x => x.Email)
.NotEmpty()
.EmailAddress();
“`

Validation nên cover:

– Required fields.
– Length.
– Format.
– Range.
– Enum.
– Business constraint cơ bản.

Không nên để invalid data đi sâu xuống database mới phát hiện.

—

# 9. Validation khác Business Rule

Điểm này rất quan trọng.

Validation:

“`text
Email không được null
“`

Business rule:

“`text
Email không được trùng
“`

Validation thường có thể kiểm tra ngay từ request.

Business rule thường cần query DB hoặc logic domain.

Ví dụ:

“`csharp
if (await _userRepository.ExistsByEmailAsync(request.Email))
{
throw new ConflictException(“Email already exists”);
}
“`

Đừng nhồi tất cả logic vào validator.

—

# 10. Controller nên mỏng

Một controller không nên chứa toàn bộ business logic.

Không nên:

“`csharp
[HttpPost]
public async Task Create(CreateUserRequest request)
{
var exists = await _dbContext.Users
.AnyAsync(x => x.Email == request.Email);

if (exists)
return BadRequest();

var user = new User
{
Id = Guid.NewGuid(),
Name = request.Name,
Email = request.Email
};

_dbContext.Users.Add(user);

await _dbContext.SaveChangesAsync();

// send email
// publish event
// create profile
// logging
// notification

return Ok();
}
“`

Controller nên làm chủ yếu:

“`text
Receive request
↓
Validate
↓
Call application/service layer
↓
Return response
“`

Ví dụ:

“`csharp
[HttpPost]
public async Task Create(
CreateUserRequest request,
CancellationToken cancellationToken)
{
var result = await _userService.CreateAsync(
request,
cancellationToken);

return CreatedAtAction(
nameof(GetById),
new { id = result.Id },
result);
}
“`

—

# 11. Không tạo Repository chỉ để wrap DbSet một cách máy móc

Ví dụ:

“`csharp
Task GetById(Guid id);
Task Add(User user);
Task Update(User user);
Task Delete(User user);
“`

Nếu implementation chỉ là:

“`csharp
_dbContext.Users.FindAsync(id)
“`

thì đôi khi abstraction này không mang lại nhiều giá trị.

EF Core `DbContext` vốn đã có nhiều đặc điểm của:

“`text
Unit of Work
+
Repository
“`

Repository nên được dùng khi nó giúp encapsulate:

– Complex query.
– Domain-specific persistence logic.
– Reusable data access.
– Aggregate boundaries.

Không nên tạo abstraction chỉ vì pattern.

—

# 12. Sử dụng AsNoTracking cho read-only query

Ví dụ:

“`csharp
var users = await _dbContext.Users
.AsNoTracking()
.ToListAsync();
“`

Nếu không update entity sau đó thì không cần change tracking.

Điều này giúp giảm:

– Memory usage.
– Tracking overhead.

Đặc biệt hữu ích cho API list/read-only.

—

# 13. Project trực tiếp sang DTO

Không nên:

“`csharp
var users = await _dbContext.Users
.ToListAsync();

var response = users.Select(x => new UserResponse
{
Id = x.Id,
Name = x.Name
});
“`

Nếu chỉ cần hai field.

Nên:

“`csharp
var response = await _dbContext.Users
.AsNoTracking()
.Select(x => new UserResponse
{
Id = x.Id,
Name = x.Name
})
.ToListAsync();
“`

Database chỉ trả dữ liệu cần thiết.

Ví dụ thay vì:

“`sql
SELECT *
“`

có thể thành:

“`sql
SELECT id, name
“`

—

# 14. Tránh N+1 Query

Một lỗi performance phổ biến.

Ví dụ:

“`csharp
var users = await db.Users.ToListAsync();

foreach (var user in users)
{
var orders = await db.Orders
.Where(x => x.UserId == user.Id)
.ToListAsync();
}
“`

Nếu có:

“`text
100 users
“`

có thể tạo:

“`text
1 query lấy users
+
100 query lấy orders
=
101 queries
“`

Đây là:

> N+1 Query Problem.

Nên cân nhắc:

– Projection.
– Join.
– Include.
– Batch query.

tùy use case.

—

# 15. Pagination là bắt buộc với list API lớn

Không nên:

“`http
GET /api/users
“`

rồi trả:

“`text
1,000,000 users
“`

Nên:

“`http
GET /api/users?page=1&pageSize=20
“`

Response có thể:

“`json
{
“items”: [],
“page”: 1,
“pageSize”: 20,
“total”: 5000
}
“`

Hoặc với hệ thống lớn có thể dùng cursor pagination.

Ví dụ:

“`http
GET /api/users?cursor=abc123&limit=20
“`

Cursor pagination phù hợp hơn khi dữ liệu thay đổi liên tục hoặc dataset rất lớn.

—

# 16. Giới hạn page size

Client không nên được phép gửi:

“`http
?pageSize=1000000
“`

Nên giới hạn:

“`csharp
pageSize = Math.Min(pageSize, 100);
“`

Ví dụ:

“`text
Default page size = 20
Maximum page size = 100
“`

để bảo vệ API và database.

—

# 17. Filter và sort phải whitelist

Không nên cho client gửi tùy ý:

“`http
?sort=DROP TABLE users
“`

hoặc dynamic field không kiểm soát.

Nên whitelist:

“`text
created_at
updated_at
name
“`

Ví dụ:

“`csharp
query = request.SortBy switch
{
“name” => query.OrderBy(x => x.Name),
“created_at” => query.OrderBy(x => x.CreatedAt),
_ => query.OrderByDescending(x => x.CreatedAt)
};
“`

—

# 18. Authentication và Authorization là hai thứ khác nhau

Authentication trả lời:

> Bạn là ai?

Authorization trả lời:

> Bạn được phép làm gì?

Ví dụ:

“`text
JWT valid
“`

không có nghĩa user được gọi mọi endpoint.

Có thể dùng:

“`csharp
[Authorize]
“`

và:

“`csharp
[Authorize(Roles = “admin”)]
“`

Hoặc tốt hơn ở hệ thống phức tạp:

“`text
Policy-based authorization
“`

Ví dụ:

“`csharp
[Authorize(Policy = “CanManageUsers”)]
“`

—

# 19. Không tin dữ liệu từ client

Ví dụ client gửi:

“`json
{
“userId”: “admin-user-id”
}
“`

Nếu authenticated user ID đã nằm trong token thì backend không nên tin `userId` từ body.

Nên lấy từ claim:

“`csharp
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
“`

Rule:

> Security-sensitive identity nên được lấy từ trusted server-side context, không lấy từ request body.

—

# 20. Không expose stack trace cho client

Không nên trả:

“`json
{
“error”: “NullReferenceException at UserService.cs line 125…”
}
“`

Điều này:

– Leak implementation detail.
– Có risk security.
– Client cũng không cần biết.

Production nên trả:

“`json
{
“code”: “INTERNAL_SERVER_ERROR”,
“message”: “An unexpected error occurred.”
}
“`

Stack trace ghi vào log.

—

# 21. Global Exception Handler

Không nên:

“`csharp
try
{
}
catch(Exception ex)
{
}
“`

ở từng controller.

Nên dùng centralized exception handling.

Trong ASP.NET Core có thể sử dụng:

“`text
Exception Handler Middleware
“`

hoặc:

“`text
IExceptionHandler
“`

Map exception:

“`text
ValidationException
-> 400

NotFoundException
-> 404

ConflictException
-> 409

UnauthorizedException
-> 401

Exception
-> 500
“`

Điều này giúp response nhất quán.

—

# 22. Không swallow exception

Một anti-pattern:

“`csharp
try
{
await DoSomething();
}
catch (Exception)
{
}
“`

Hoặc:

“`csharp
catch (Exception ex)
{
return null;
}
“`

Lỗi bị mất hoàn toàn.

Nếu catch thì phải có lý do rõ ràng:

– Handle.
– Retry.
– Translate exception.
– Add context.
– Log.

Nếu không xử lý được thì để exception propagate tới global handler.

—

# 23. Logging có structure

Không nên:

“`csharp
_logger.LogInformation(
“User ” + userId + ” created order ” + orderId);
“`

Nên:

“`csharp
_logger.LogInformation(
“User {UserId} created order {OrderId}”,
userId,
orderId);
“`

Structured logging giúp search dễ hơn:

“`text
UserId = xxx
OrderId = yyy
“`

trong Seq, Application Insights, Elasticsearch hoặc các hệ thống log khác.

—

# 24. Không log sensitive data

Không nên log:

“`text
Password
Access Token
Refresh Token
Credit Card
OTP
Secret Key
Authorization Header
“`

Ví dụ cực kỳ nguy hiểm:

“`csharp
_logger.LogInformation(
“Login request {@Request}”,
request);
“`

nếu request chứa password.

Logging phải có data classification rõ ràng.

—

# 25. Correlation ID và Trace ID

Khi một request đi qua:

“`text
API Gateway
↓
Auth Service
↓
Order Service
↓
Payment Service
↓
Notification Service
“`

cần cách trace toàn bộ flow.

Nên có:

“`text
TraceId
CorrelationId
“`

Ví dụ log:

“`text
TraceId = 8bf92…

User API
Payment API
Notification Worker
“`

Từ đó có thể reconstruct toàn bộ request flow.

OpenTelemetry là một lựa chọn phổ biến cho việc này.

—

# 26. CancellationToken

Một best practice thường bị bỏ qua:

“`csharp
public async Task Get(
CancellationToken cancellationToken)
{
var data = await _service.GetAsync(
cancellationToken);

return Ok(data);
}
“`

Sau đó truyền xuống:

“`csharp
await db.Users
.ToListAsync(cancellationToken);
“`

Nếu client disconnect:

“`text
Browser
X
Request cancelled
“`

server có cơ hội dừng công việc không còn cần thiết.

—

# 27. Async phải async xuyên suốt

Không nên:

“`csharp
var result = _service.GetAsync().Result;
“`

hoặc:

“`csharp
_service.GetAsync().Wait();
“`

Nên:

“`csharp
var result = await _service.GetAsync();
“`

Nguyên tắc:

> Async all the way.

Điều này đặc biệt quan trọng với I/O:

– Database.
– HTTP call.
– File.
– Message broker.

—

# 28. Không dùng async chỉ để chạy CPU work

`async/await` chủ yếu giúp với I/O-bound work.

Ví dụ:

“`text
Database
HTTP
File
Network
“`

Không phải:

“`text
Heavy CPU calculation
“`

Nếu API có CPU-intensive task lớn, cần cân nhắc:

– Background worker.
– Queue.
– Dedicated processing service.

—

# 29. Transaction chỉ nên giữ ngắn

Không nên:

“`text
BEGIN TRANSACTION
↓
Query DB
↓
Call third-party API 20 seconds
↓
Send email
↓
Update DB
↓
COMMIT
“`

Transaction mở quá lâu có thể gây:

– Lock.
– Blocking.
– Deadlock.
– Connection exhaustion.

Nên giữ DB transaction càng ngắn càng tốt.

Ví dụ:

“`text
Validate
↓
External work nếu phù hợp
↓
BEGIN
DB changes
COMMIT
“`

Tùy business consistency requirement.

—

# 30. Không dùng distributed transaction một cách tùy tiện

Nếu flow gồm:

“`text
Database
+
Kafka
+
Email
+
External Payment API
“`

một DB transaction thông thường không thể rollback tất cả.

Nên cân nhắc các pattern:

“`text
Transactional Outbox

Saga

Idempotency

Compensation
“`

thay vì cố tạo một transaction khổng lồ.

—

# 31. Idempotency rất quan trọng với POST

Giả sử client gọi:

“`http
POST /payments
“`

Request thành công nhưng client timeout.

Client retry:

“`http
POST /payments
“`

Nếu backend xử lý lại:

“`text
Payment 1
Payment 2
“`

user có thể bị charge hai lần.

Có thể dùng:

“`http
Idempotency-Key: abc123
“`

Backend lưu key:

“`text
abc123 -> Payment 789
“`

Nếu request retry với cùng key:

“`text
return previous result
“`

thay vì tạo transaction mới.

—

# 32. Timeout khi gọi external API

Không nên gọi:

“`text
Third-party API
“`

mà không có timeout.

Nếu third party treo:

“`text
Request
↓
wait forever
“`

server thread/request có thể bị giữ rất lâu.

Nên configure timeout rõ ràng.

—

# 33. Retry phải có kiểm soát

Retry không phải lúc nào cũng tốt.

Ví dụ lỗi:

“`text
400 Bad Request
“`

retry 10 lần cũng không giúp gì.

Retry nên dành cho transient failures:

“`text
Timeout

503 Service Unavailable

temporary network error
“`

Nên dùng:

“`text
Exponential Backoff
+
Jitter
“`

để tránh retry storm.

—

# 34. Circuit Breaker

Nếu external service đã down:

“`text
Request 1 -> fail
Request 2 -> fail
Request 3 -> fail
…
“`

tiếp tục gửi request có thể làm hệ thống tệ hơn.

Circuit Breaker có thể:

“`text
Closed
↓
Failures exceed threshold
↓
Open
↓
Stop calling temporarily
↓
Half Open
↓
Test recovery
“`

Trong .NET có thể triển khai resilience thông qua HttpClient resilience pipeline.

—

# 35. Dùng HttpClientFactory

Không nên:

“`csharp
using var client = new HttpClient();
“`

cho từng request.

Nên sử dụng:

“`csharp
IHttpClientFactory
“`

Ví dụ:

“`csharp
services.AddHttpClient();
“`

Điều này giúp quản lý:

– Connection pooling.
– DNS refresh.
– Configuration.
– Resilience policies.

—

# 36. API Versioning

API sẽ thay đổi.

Ví dụ:

“`http
/api/v1/users
“`

sau này:

“`http
/api/v2/users
“`

Versioning giúp client cũ không bị phá ngay khi contract thay đổi.

Có nhiều chiến lược:

“`text
URL versioning
Header versioning
Query versioning
“`

URL versioning dễ hiểu:

“`http
/v1/users
/v2/users
“`

Điều quan trọng không phải chọn kiểu nào, mà là consistency.

—

# 37. Tránh breaking change

Ví dụ response hiện tại:

“`json
{
“name”: “Vu”
}
“`

Sau đó đổi:

“`json
{
“fullName”: “Vu”
}
“`

Client cũ có thể break.

Breaking changes thường gồm:

– Rename field.
– Remove field.
– Change data type.
– Change nullable thành required.
– Change semantic.
– Change enum value.

Nếu thay đổi lớn, nên cân nhắc version mới.

—

# 38. Enum trong API cần cẩn thận

Ví dụ:

“`json
{
“status”: 1
}
“`

Client không biết `1` là gì.

Có thể dùng:

“`json
{
“status”: “active”
}
“`

dễ đọc hơn.

Nhưng khi thêm giá trị enum mới, client cũ cũng cần có strategy xử lý unknown values.

—

# 39. DateTime nên có timezone rõ ràng

Không nên trả:

“`json
{
“createdAt”: “2026-09-30 15:30:00”
}
“`

Không biết timezone gì.

Nên dùng ISO 8601:

“`json
{
“createdAt”: “2026-09-30T08:30:00Z”
}
“`

hoặc offset:

“`json
{
“createdAt”: “2026-09-30T15:30:00+07:00”
}
“`

Một rule phổ biến:

> Lưu UTC, convert theo timezone khi hiển thị hoặc theo business requirement.

—

# 40. Database index theo access pattern

Không nên index mọi column.

Index nên dựa trên query thực tế.

Ví dụ thường query:

“`sql
WHERE OrganizationId = @id
AND DeletedAt IS NULL
ORDER BY CreatedAt DESC
“`

thì nên phân tích execution plan và cân nhắc composite index phù hợp.

Index có trade-off:

“`text
Read faster
BUT
Insert/Update/Delete more expensive
+
more storage
“`

—

# 41. Không query nhiều data rồi filter trong memory

Không nên:

“`csharp
var users = await db.Users.ToListAsync();

var activeUsers = users
.Where(x => x.Status == Active)
.ToList();
“`

Nên:

“`csharp
var activeUsers = await db.Users
.Where(x => x.Status == Active)
.ToListAsync();
“`

Để database filter.

Rule:

> Push filtering, sorting, pagination và projection xuống database càng nhiều càng tốt.

—

# 42. Health Check

Production API nên có health endpoint.

Ví dụ:

“`http
/health
“`

Có thể kiểm tra:

“`text
Application alive
Database
Redis
Message Broker
Critical dependencies
“`

Nhưng cần phân biệt:

“`text
Liveness
Readiness
“`

Liveness:

> Process còn sống không?

Readiness:

> Service có sẵn sàng nhận traffic không?

—

# 43. Rate Limiting

Public API nên có rate limiting.

Ví dụ:

“`text
100 requests / minute / user
“`

hoặc:

“`text
1000 requests / minute / API key
“`

Điều này giúp bảo vệ hệ thống khỏi:

– Abuse.
– Bot.
– Accidental loop.
– Traffic spike.

ASP.NET Core có built-in rate limiting middleware.

—

# 44. Swagger/OpenAPI

API nên có documentation rõ ràng.

Swagger/OpenAPI giúp mô tả:

“`text
Endpoint
Request
Response
Authentication
Status Codes
Schema
“`

Nhưng production cần cân nhắc:

– Có public Swagger hay không.
– Environment nào được access.
– Security của API docs.

—

# 45. Không hard-code secret

Không nên:

“`csharp
var apiKey = “abc-secret-123”;
“`

trong source code.

Nên dùng:

“`text
Environment Variables

Azure Key Vault

AWS Secrets Manager

User Secrets cho local development
“`

Secret không nên commit vào Git.

—

# 46. Cache đúng chỗ

Không phải API nào cũng cần cache.

Cache phù hợp với:

– Read-heavy.
– Dữ liệu ít thay đổi.
– Expensive query.
– External API data.

Có thể dùng:

“`text
Memory Cache

Redis

HTTP Cache
“`

Nhưng cần nghĩ trước:

“`text
Cache invalidation
TTL
Consistency
“`

—

# 47. API không nên phụ thuộc trực tiếp vào UI

Ví dụ backend trả:

“`json
{
“buttonColor”: “red”,
“buttonPosition”: “left”
}
“`

trừ khi API thực sự phục vụ remote configuration/UI configuration.

Business API nên trả business state:

“`json
{
“status”: “expired”
}
“`

UI tự quyết:

“`text
expired
↓
display red
“`

Điều này giảm coupling giữa frontend và backend.

—

# 48. Naming phải nhất quán

Ví dụ không nên:

“`text
/users/{id}

/user-detail/{user_id}

/GetUserById
“`

trong cùng hệ thống.

Chọn convention:

“`text
kebab-case URL

camelCase JSON
“`

Ví dụ:

“`http
GET /api/user-notifications
“`

Response:

“`json
{
“createdAt”: “…”,
“notificationType”: “event_reminder”
}
“`

Consistency quan trọng hơn sở thích cá nhân.

—

# 49. Một architecture đơn giản nhưng tốt

Không cần hệ thống nào cũng phải Clean Architecture 10 project.

Một Web API vừa phải có thể:

“`text
API
↓
Application / Service
↓
Domain
↓
Infrastructure
↓
Database
“`

Flow:

“`text
Controller
↓
Application Service / Handler
↓
Domain Logic
↓
Repository / DbContext
↓
Database
“`

Điều quan trọng là:

> Dependency và responsibility phải rõ ràng.

Không phải càng nhiều layer càng tốt.

—

# 50. Checklist Web API thực tế

Trước khi đưa một API lên production, có thể kiểm tra:

“`text
[ ] HTTP methods đúng chưa?

[ ] Status codes đúng chưa?

[ ] Input validation có chưa?

[ ] Authentication có chưa?

[ ] Authorization có đúng resource không?

[ ] DTO tách khỏi Entity chưa?

[ ] Pagination có chưa?

[ ] Query có AsNoTracking chưa?

[ ] Có N+1 query không?

[ ] Index phù hợp chưa?

[ ] Exception được handle centrally chưa?

[ ] Log có structured không?

[ ] Có leak sensitive data không?

[ ] CancellationToken có propagate chưa?

[ ] External API có timeout không?

[ ] Retry có đúng transient error không?

[ ] POST quan trọng có idempotency chưa?

[ ] TraceId có chưa?

[ ] Health check có chưa?

[ ] Rate limiting có cần không?

[ ] API contract có breaking change không?
“`

—

# 51. Câu trả lời khi phỏng vấn

Nếu interviewer hỏi:

> What are your best practices when designing a Web API?

Không nên chỉ trả lời:

> I use REST and JWT.

Một câu trả lời tốt hơn:

> I start with a consistent resource-oriented API contract, correct HTTP methods and status codes, and separate request and response DTOs from persistence entities. I validate requests at the boundary and keep controllers thin, while business logic stays in the application or domain layer.
>
> For data access, I avoid N+1 queries, use projection, pagination and proper indexes, and use no-tracking queries for read-only operations. For production reliability, I use centralized exception handling, structured logging, tracing, cancellation tokens, timeouts and controlled retry policies.
>
> For security, I separate authentication from authorization, never trust client-provided identity information, avoid exposing sensitive data, and manage secrets outside source code. I also consider idempotency, API versioning and backward compatibility for endpoints that are used by external clients.

Câu trả lời này thể hiện bạn không chỉ biết syntax mà còn hiểu production system.

—

# Kết luận

Một Web API tốt không được đánh giá chỉ dựa trên việc:

“`text
request vào
↓
database query
↓
return JSON
“`

Một API production-ready cần quan tâm cả vòng đời:

“`text
Client
↓
Authentication
↓
Authorization
↓
Validation
↓
Application Logic
↓
Database
↓
External Services
↓
Response
↓
Logging
↓
Tracing
↓
Monitoring
“`

Những nguyên tắc quan trọng nhất có thể tóm tắt như sau:

“`text
Design API around resources

Use HTTP semantics correctly

Keep controllers thin

Separate DTO from Entity

Validate early

Paginate large datasets

Optimize database queries

Avoid N+1

Handle errors centrally

Use structured logging

Protect sensitive data

Use async correctly

Propagate CancellationToken

Keep transactions short

Add idempotency where necessary

Use timeout and controlled retries

Version APIs carefully

Avoid unnecessary abstractions

Measure before optimizing
“`

Cuối cùng, best practice không phải là áp dụng mọi pattern vào mọi project.

> Best practice tốt nhất là lựa chọn architecture và pattern phù hợp với độ phức tạp thực tế của hệ thống, đồng thời giữ API nhất quán, dễ hiểu và dễ vận hành trong production.