api-design
SKILL.md
REST API Design
Expert API design agent for EasyPlatform following platform patterns and REST best practices.
Controller Pattern
[ApiController]
[Route("api/[controller]")]
[PlatformAuthorize] // Require authentication
public class EmployeeController : PlatformBaseController
{
// GET api/employee - List with filtering
[HttpGet]
public async Task<IActionResult> GetList([FromQuery] GetEmployeeListQuery query)
=> Ok(await Cqrs.SendAsync(query));
// GET api/employee/{id} - Single by ID
[HttpGet("{id}")]
public async Task<IActionResult> GetById(string id)
=> Ok(await Cqrs.SendAsync(new GetEmployeeByIdQuery { Id = id }));
// POST api/employee - Create/Update
[HttpPost]
[PlatformAuthorize(Roles = "Admin,Manager")]
public async Task<IActionResult> Save([FromBody] SaveEmployeeCommand command)
=> Ok(await Cqrs.SendAsync(command));
// DELETE api/employee/{id} - Delete
[HttpDelete("{id}")]
[PlatformAuthorize(Roles = "Admin")]
public async Task<IActionResult> Delete(string id)
=> Ok(await Cqrs.SendAsync(new DeleteEmployeeCommand { Id = id }));
// POST api/employee/search - Complex search
[HttpPost("search")]
public async Task<IActionResult> Search([FromBody] SearchEmployeesQuery query)
=> Ok(await Cqrs.SendAsync(query));
// POST api/employee/{id}/action - Custom action
[HttpPost("{id}/activate")]
public async Task<IActionResult> Activate(string id)
=> Ok(await Cqrs.SendAsync(new ActivateEmployeeCommand { Id = id }));
}
Route Naming Conventions
| Action | HTTP Method | Route Pattern | Example |
|---|---|---|---|
| List | GET | /api/{resource} |
GET /api/employees |
| Get by ID | GET | /api/{resource}/{id} |
GET /api/employees/123 |
| Create/Update | POST | /api/{resource} |
POST /api/employees |
| Delete | DELETE | /api/{resource}/{id} |
DELETE /api/employees/123 |
| Complex Search | POST | /api/{resource}/search |
POST /api/employees/search |
| Custom Action | POST | /api/{resource}/{id}/{action} |
POST /api/employees/123/activate |
| Nested Resource | GET | /api/{parent}/{id}/{child} |
GET /api/departments/1/employees |
Request/Response DTOs
Query DTO (GET requests)
// Simple query params - use record
public record GetEmployeeListQuery : PlatformCqrsPagedQuery<GetEmployeeListQueryResult, EmployeeDto>
{
public List<EmploymentStatus>? Statuses { get; init; }
public string? SearchText { get; init; }
public string? DepartmentId { get; init; }
}
Command DTO (POST/PUT/DELETE)
public sealed class SaveEmployeeCommand : PlatformCqrsCommand<SaveEmployeeCommandResult>
{
public string? Id { get; set; } // Null for create
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public override PlatformValidationResult<IPlatformCqrsRequest> Validate()
{
return base.Validate()
.And(_ => FirstName.IsNotNullOrEmpty(), "FirstName is required")
.And(_ => Email.IsNotNullOrEmpty(), "Email is required")
.And(_ => Email.Contains("@"), "Invalid email format");
}
}
Response DTO
// For single entity
public sealed class SaveEmployeeCommandResult : PlatformCqrsCommandResult
{
public EmployeeDto Entity { get; set; } = null!;
}
// For paged list
public sealed class GetEmployeeListQueryResult : PlatformCqrsPagedQueryResult<EmployeeDto>
{
public Dictionary<EmploymentStatus, int> StatusCounts { get; set; } = new();
public GetEmployeeListQueryResult(List<Employee> items, int total, GetEmployeeListQuery req, Dictionary<EmploymentStatus, int> counts)
: base(items.SelectList(e => new EmployeeDto(e)), total, req)
{
StatusCounts = counts;
}
}
Authorization Patterns
// Controller level - all endpoints
[PlatformAuthorize]
public class SecureController : PlatformBaseController { }
// Endpoint level - specific roles
[HttpPost]
[PlatformAuthorize(Roles = "Admin,Manager")]
public async Task<IActionResult> AdminOnly() { }
// Handler level - business validation
protected override async Task<PlatformValidationResult<T>> ValidateRequestAsync(...)
{
return await validation
.And(_ => RequestContext.HasRole("Admin") || RequestContext.UserId() == req.TargetUserId,
"Can only modify own data or be Admin");
}
File Upload Endpoints
[HttpPost("upload")]
[RequestSizeLimit(50 * 1024 * 1024)] // 50MB
public async Task<IActionResult> Upload([FromForm] UploadCommand command)
=> Ok(await Cqrs.SendAsync(command));
public sealed class UploadCommand : PlatformCqrsCommand<UploadCommandResult>
{
[FromForm]
public IFormFile File { get; set; } = null!;
[FromForm]
public string? Description { get; set; }
}
Error Response Format
// Platform handles errors automatically with standard format
{
"type": "validation",
"title": "Validation Error",
"status": 400,
"errors": {
"email": ["Email is required", "Invalid email format"],
"firstName": ["FirstName is required"]
}
}
// Business errors
{
"type": "business",
"title": "Business Rule Violation",
"status": 422,
"detail": "Employee is already assigned to this department"
}
API Design Checklist
- RESTful route naming (plural nouns, lowercase)?
- Appropriate HTTP methods?
- Proper authorization attributes?
- Validation in Command/Query Validate()?
- Consistent response format?
- Paging for list endpoints?
- Error handling follows platform patterns?
Anti-Patterns
- Verbs in URLs: Use
/employees/123/activatenot/activateEmployee - Missing Authorization: Always add
[PlatformAuthorize] - Validation in Controller: Move to Command/Query
Validate() - Business Logic in Controller: Keep controllers thin, logic in handlers
- Inconsistent Naming: Follow
{Resource}Controllerpattern
Task Planning Notes
- Always plan and break many small todo tasks
- Always add a final review todo task to review the works done at the end to find any fix or enhancement needed
Weekly Installs
5
Repository
duc01226/easyplatformFirst Seen
Jan 24, 2026
Security Audits
Installed on
claude-code4
windsurf3
opencode3
codex3
antigravity3
gemini-cli3