Agent Skills: REST API Design

[Implementation] Use when designing or modifying REST API endpoints, controller structure, route patterns, request/response DTOs. Triggers on keywords like "API endpoint", "REST", "controller", "route", "HTTP", "request body", "response".

UncategorizedID: duc01226/easyplatform/api-design

Install this agent skill to your local

pnpm dlx add-skill https://github.com/duc01226/EasyPlatform/tree/HEAD/.claude/skills/api-design

Skill Files

Browse the full folder contents for api-design.

Download Skill

Loading file tree…

.claude/skills/api-design/SKILL.md

Skill Metadata

Name
api-design
Description
"[Implementation] Use when designing or modifying REST API endpoints, controller structure, route patterns, request/response DTOs. Triggers on keywords like "API endpoint", "REST", "controller", "route", "HTTP", "request body", "response"."

REST API Design

Expert API design agent for EasyPlatform following platform patterns and REST best practices.

Summary

Goal: Design REST API endpoints following EasyPlatform controller patterns with proper routing, DTOs, authorization, and CQRS integration.

| Step | Action | Key Notes | |------|--------|-----------| | 1 | Define routes | RESTful naming: GET list, GET by ID, POST save, DELETE | | 2 | Create DTOs | Request/response DTOs with validation; DTO owns mapping | | 3 | Wire CQRS | Controller delegates to Cqrs.SendAsync(command/query) | | 4 | Add authorization | [PlatformAuthorize] at controller + role-level per action | | 5 | Document | OpenAPI attributes, response types |

Key Principles:

  • Controllers are thin — delegate all logic to CQRS commands/queries
  • Use PlatformBaseController as base class with Cqrs property
  • POST for create/update (not PUT); custom actions as POST {id}/action

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/activate not /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}Controller pattern

IMPORTANT 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