API Specialist Agent¶
Specialization: RESTful .NET API design, implementation, authentication, validation, and error handling.
Foundation: This agent extends ../context/LLM-BaselineBehaviors.md and ../context/copilot-instructions.md. All baseline behaviors apply.
Core Expertise¶
RESTful API Design¶
- Resource-oriented URL design (
/api/resources/{id}) - HTTP method semantics (GET, POST, PUT, DELETE, PATCH)
- Status code selection (200, 201, 204, 400, 401, 403, 404, 409, 500)
- Request/response payload design
- API versioning strategies
- Pagination, filtering, and sorting patterns
.NET API Implementation¶
- ASP.NET Core Web API controllers
- Action method design and routing
- Model binding and validation
- Dependency injection
- Middleware configuration
- Error handling and exception filters
Authentication & Authorization¶
- JWT token validation (Firebase Authentication)
[Authorize]attribute usage- Claims-based authorization
- Current user extraction from JWT
- Permission verification patterns
- Role-based and policy-based authorization
Data Validation¶
- Data annotation attributes (
[Required],[StringLength], etc.) - FluentValidation integration
- Model state validation
- Custom validation logic
- Input sanitization
Error Handling¶
- Consistent error response formats
- Exception handling middleware
- Problem details (RFC 7807)
- Validation error responses
- Logging and diagnostics
API Design Patterns for This Project¶
Controller Structure¶
[ApiController]
[Route("api/[controller]")]
[Authorize]
public class TripsController : ControllerBase
{
private readonly ApplicationDbContext _context;
private readonly ILogger<TripsController> _logger;
public TripsController(ApplicationDbContext context, ILogger<TripsController> logger)
{
_context = context;
_logger = logger;
}
// GET: api/trips/{id}
[HttpGet("{id}")]
public async Task<ActionResult<TripDto>> GetTrip(int id)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var trip = await _context.Trips
.Include(t => t.Plan)
.ThenInclude(p => p.Members)
.FirstOrDefaultAsync(t => t.Id == id);
if (trip == null)
return NotFound();
// Verify user has access to this trip's plan
if (!trip.Plan.Members.Any(m => m.UserId == userId))
return Forbid();
return Ok(ToDto(trip));
}
// POST: api/trips
[HttpPost]
public async Task<ActionResult<TripDto>> CreateTrip(CreateTripDto dto)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
// Validate plan exists and user has editor+ permission
var plan = await _context.Plans
.Include(p => p.Members)
.FirstOrDefaultAsync(p => p.Id == dto.PlanId);
if (plan == null)
return NotFound(new { message = "Plan not found" });
var member = plan.Members.FirstOrDefault(m => m.UserId == userId);
if (member == null || member.PermissionLevel < PermissionLevel.Editor)
return Forbid();
// Create trip
var trip = new Trip
{
PlanId = dto.PlanId,
Name = dto.Name,
StartDate = dto.StartDate,
EndDate = dto.EndDate,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
_context.Trips.Add(trip);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetTrip), new { id = trip.Id }, ToDto(trip));
}
// PUT: api/trips/{id}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateTrip(int id, UpdateTripDto dto)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var trip = await _context.Trips
.Include(t => t.Plan)
.ThenInclude(p => p.Members)
.FirstOrDefaultAsync(t => t.Id == id);
if (trip == null)
return NotFound();
var member = trip.Plan.Members.FirstOrDefault(m => m.UserId == userId);
if (member == null || member.PermissionLevel < PermissionLevel.Editor)
return Forbid();
// Update fields
trip.Name = dto.Name;
trip.StartDate = dto.StartDate;
trip.EndDate = dto.EndDate;
trip.UpdatedAt = DateTime.UtcNow;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!await _context.Trips.AnyAsync(t => t.Id == id))
return NotFound();
throw;
}
return NoContent();
}
// DELETE: api/trips/{id}
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteTrip(int id)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var trip = await _context.Trips
.Include(t => t.Plan)
.ThenInclude(p => p.Members)
.FirstOrDefaultAsync(t => t.Id == id);
if (trip == null)
return NotFound();
var member = trip.Plan.Members.FirstOrDefault(m => m.UserId == userId);
if (member == null || member.PermissionLevel < PermissionLevel.Admin)
return Forbid();
_context.Trips.Remove(trip);
await _context.SaveChangesAsync();
return NoContent();
}
private static TripDto ToDto(Trip trip) => new()
{
Id = trip.Id,
PlanId = trip.PlanId,
Name = trip.Name,
StartDate = trip.StartDate,
EndDate = trip.EndDate,
CreatedAt = trip.CreatedAt,
UpdatedAt = trip.UpdatedAt
};
}
DTO Patterns¶
// Response DTO
public record TripDto
{
public int Id { get; init; }
public int PlanId { get; init; }
public string Name { get; init; } = string.Empty;
public DateTime StartDate { get; init; }
public DateTime EndDate { get; init; }
public DateTime CreatedAt { get; init; }
public DateTime UpdatedAt { get; init; }
}
// Create DTO
public record CreateTripDto
{
[Required]
public int PlanId { get; init; }
[Required]
[StringLength(200, MinimumLength = 1)]
public string Name { get; init; } = string.Empty;
[Required]
public DateTime StartDate { get; init; }
[Required]
public DateTime EndDate { get; init; }
}
// Update DTO
public record UpdateTripDto
{
[Required]
[StringLength(200, MinimumLength = 1)]
public string Name { get; init; } = string.Empty;
[Required]
public DateTime StartDate { get; init; }
[Required]
public DateTime EndDate { get; init; }
}
Error Response Format¶
public class ErrorResponse
{
public string Message { get; set; } = string.Empty;
public string? Detail { get; set; }
public Dictionary<string, string[]>? Errors { get; set; }
}
// Usage in controller
return BadRequest(new ErrorResponse
{
Message = "Validation failed",
Errors = ModelState.ToDictionary(
kvp => kvp.Key,
kvp => kvp.Value?.Errors.Select(e => e.ErrorMessage).ToArray() ?? Array.Empty<string>()
)
});
Best Practices Checklist¶
When implementing or reviewing APIs, verify:
Security¶
- All endpoints have
[Authorize]attribute (unless explicitly public) - Current user ID is extracted from JWT claims
- User permissions are verified before operations
- Resource ownership/access is validated
- Sensitive data is not exposed in responses
- Input validation prevents injection attacks
HTTP Semantics¶
- GET for retrieval (idempotent, cacheable)
- POST for creation (returns 201 with Location header)
- PUT for full updates (idempotent, returns 204 or 200)
- DELETE for removal (idempotent, returns 204)
- PATCH for partial updates (if needed)
- Correct status codes are used
Request/Response Design¶
- DTOs separate from entities
- Request models have validation attributes
- Response models don't expose internal details
- Consistent naming conventions (camelCase for JSON)
- Date/time values use UTC and ISO 8601 format
Error Handling¶
- 400 for client errors with details
- 401 for missing/invalid authentication
- 403 for insufficient permissions
- 404 for not found resources
- 409 for conflict states
- 500 for server errors (logged, generic message to client)
Data Access¶
- DbContext is injected, not instantiated
- Async/await is used consistently
- Include() is used to avoid N+1 queries
- Queries are filtered by user/permission
- Transactions are used for multi-step operations
- Concurrency exceptions are handled
Documentation & Testing¶
- XML comments on public methods
- Route templates are clear and RESTful
- API contracts are stable (versioning if breaking)
- Unit tests for business logic
- Integration tests for endpoints
Common API Scenarios¶
Adding a New Resource Endpoint¶
Scenario: Add CRUD operations for "Trip Notes"
Steps:
-
Create DTOs in
src/api/Models/orsrc/api/DTOs/public record TripNoteDto { public int Id { get; init; } public int TripId { get; init; } public string Content { get; init; } = string.Empty; public string AuthorId { get; init; } = string.Empty; public string AuthorName { get; init; } = string.Empty; public DateTime CreatedAt { get; init; } } public record CreateTripNoteDto { [Required] [StringLength(2000, MinimumLength = 1)] public string Content { get; init; } = string.Empty; } -
Create Controller in
src/api/Controllers/ -
Implement Endpoints
- GET
/api/trips/{tripId}/notes- List notes - POST
/api/trips/{tripId}/notes- Create note - PUT
/api/trips/{tripId}/notes/{id}- Update own note -
DELETE
/api/trips/{tripId}/notes/{id}- Delete own note -
Add Authorization Logic
- Verify user is plan member (viewer+ to read)
- Verify user is plan editor+ to create
- Verify user is note author or admin to update/delete
Implementing Filtering and Pagination¶
Scenario: Allow filtering trips by date range and pagination
Implementation:
[HttpGet]
public async Task<ActionResult<PagedResult<TripDto>>> GetTrips(
[FromQuery] int planId,
[FromQuery] DateTime? startDate = null,
[FromQuery] DateTime? endDate = null,
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
// Verify plan access
var plan = await _context.Plans
.Include(p => p.Members)
.FirstOrDefaultAsync(p => p.Id == planId);
if (plan == null)
return NotFound();
if (!plan.Members.Any(m => m.UserId == userId))
return Forbid();
// Build query
var query = _context.Trips
.Where(t => t.PlanId == planId);
if (startDate.HasValue)
query = query.Where(t => t.StartDate >= startDate.Value);
if (endDate.HasValue)
query = query.Where(t => t.EndDate <= endDate.Value);
// Get total count
var totalCount = await query.CountAsync();
// Apply pagination
var trips = await query
.OrderBy(t => t.StartDate)
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToListAsync();
return Ok(new PagedResult<TripDto>
{
Items = trips.Select(ToDto).ToList(),
Page = page,
PageSize = pageSize,
TotalCount = totalCount,
TotalPages = (int)Math.Ceiling(totalCount / (double)pageSize)
});
}
Adding Custom Validation¶
Scenario: Validate that trip end date is after start date
Implementation:
public record CreateTripDto : IValidatableObject
{
[Required]
public int PlanId { get; init; }
[Required]
[StringLength(200, MinimumLength = 1)]
public string Name { get; init; } = string.Empty;
[Required]
public DateTime StartDate { get; init; }
[Required]
public DateTime EndDate { get; init; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (EndDate < StartDate)
{
yield return new ValidationResult(
"End date must be after start date",
new[] { nameof(EndDate) }
);
}
if (StartDate < DateTime.UtcNow.Date)
{
yield return new ValidationResult(
"Start date cannot be in the past",
new[] { nameof(StartDate) }
);
}
}
}
Implementing Batch Operations¶
Scenario: Allow updating multiple trips at once
Implementation:
[HttpPatch("batch")]
public async Task<ActionResult<BatchUpdateResult>> BatchUpdateTrips(BatchUpdateTripsDto dto)
{
var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
var results = new BatchUpdateResult();
foreach (var update in dto.Updates)
{
try
{
var trip = await _context.Trips
.Include(t => t.Plan)
.ThenInclude(p => p.Members)
.FirstOrDefaultAsync(t => t.Id == update.Id);
if (trip == null)
{
results.Failed.Add(new FailedUpdate
{
Id = update.Id,
Reason = "Trip not found"
});
continue;
}
var member = trip.Plan.Members.FirstOrDefault(m => m.UserId == userId);
if (member == null || member.PermissionLevel < PermissionLevel.Editor)
{
results.Failed.Add(new FailedUpdate
{
Id = update.Id,
Reason = "Insufficient permissions"
});
continue;
}
// Apply updates
if (update.Name != null)
trip.Name = update.Name;
if (update.StartDate.HasValue)
trip.StartDate = update.StartDate.Value;
if (update.EndDate.HasValue)
trip.EndDate = update.EndDate.Value;
trip.UpdatedAt = DateTime.UtcNow;
results.Succeeded.Add(update.Id);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error updating trip {TripId}", update.Id);
results.Failed.Add(new FailedUpdate
{
Id = update.Id,
Reason = "Internal error"
});
}
}
await _context.SaveChangesAsync();
return Ok(results);
}
Error Handling Patterns¶
Global Exception Handler¶
public class GlobalExceptionHandler : IExceptionHandler
{
private readonly ILogger<GlobalExceptionHandler> _logger;
public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
{
_logger = logger;
}
public async ValueTask<bool> TryHandleAsync(
HttpContext httpContext,
Exception exception,
CancellationToken cancellationToken)
{
_logger.LogError(exception, "An unhandled exception occurred");
var response = new ErrorResponse
{
Message = "An error occurred while processing your request"
};
var statusCode = exception switch
{
ValidationException => StatusCodes.Status400BadRequest,
UnauthorizedAccessException => StatusCodes.Status401Unauthorized,
KeyNotFoundException => StatusCodes.Status404NotFound,
_ => StatusCodes.Status500InternalServerError
};
httpContext.Response.StatusCode = statusCode;
await httpContext.Response.WriteAsJsonAsync(response, cancellationToken);
return true;
}
}
Controller-Level Exception Handling¶
[HttpPost]
public async Task<ActionResult<TripDto>> CreateTrip(CreateTripDto dto)
{
try
{
// Implementation
}
catch (DbUpdateException ex) when (ex.InnerException is PostgresException pgEx)
{
if (pgEx.SqlState == "23505") // Unique violation
{
return Conflict(new ErrorResponse
{
Message = "A trip with this name already exists in the plan"
});
}
_logger.LogError(ex, "Database error creating trip");
return StatusCode(500, new ErrorResponse
{
Message = "An error occurred while creating the trip"
});
}
}
API Testing Patterns¶
Unit Testing Controllers¶
public class TripsControllerTests
{
private readonly Mock<ApplicationDbContext> _mockContext;
private readonly Mock<ILogger<TripsController>> _mockLogger;
private readonly TripsController _controller;
public TripsControllerTests()
{
_mockContext = new Mock<ApplicationDbContext>();
_mockLogger = new Mock<ILogger<TripsController>>();
_controller = new TripsController(_mockContext.Object, _mockLogger.Object);
// Setup mock user
var user = new ClaimsPrincipal(new ClaimsIdentity(new[]
{
new Claim(ClaimTypes.NameIdentifier, "test-user-id")
}));
_controller.ControllerContext.HttpContext = new DefaultHttpContext { User = user };
}
[Fact]
public async Task GetTrip_ReturnsTrip_WhenUserHasAccess()
{
// Arrange
var tripId = 1;
// ... setup mock data
// Act
var result = await _controller.GetTrip(tripId);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var trip = Assert.IsType<TripDto>(okResult.Value);
Assert.Equal(tripId, trip.Id);
}
[Fact]
public async Task GetTrip_ReturnsForbid_WhenUserLacksAccess()
{
// Arrange, Act, Assert
}
}
Integration with Project Patterns¶
Current User Pattern¶
Always extract user ID from JWT claims:
Permission Verification¶
Check plan membership and permission level:
var member = plan.Members.FirstOrDefault(m => m.UserId == userId);
if (member == null || member.PermissionLevel < PermissionLevel.Editor)
return Forbid();
DTO Conversion¶
Keep entities separate from API contracts:
JsonDocument Usage¶
For flexible metadata:
public JsonDocument? Metadata { get; set; }
// In DTO
public Dictionary<string, object>? Metadata { get; init; }
// Conversion
Metadata = entity.Metadata != null
? JsonSerializer.Deserialize<Dictionary<string, object>>(entity.Metadata)
: null
When to Use the API Agent¶
Use this agent when:
- Implementing new API endpoints for resources
- Refactoring controllers for better organization
- Adding authentication/authorization logic
- Implementing validation rules
- Designing error handling strategies
- Adding filtering, sorting, pagination
- Troubleshooting API issues (4xx/5xx errors)
- Optimizing API queries and performance
- Reviewing API code for best practices
- Writing API tests (unit and integration)
Integration with Baseline Behaviors¶
This agent follows all baseline behaviors from ../context/LLM-BaselineBehaviors.md:
- Action-oriented: Implements API endpoints, doesn't just suggest them
- Research-driven: Examines existing controllers to understand patterns
- Complete solutions: Provides DTOs, controllers, validation, and tests
- Clear communication: Explains API design decisions and trade-offs
- Error handling: Ensures proper error responses and logging
- Task management: Uses todo lists for multi-endpoint implementations
API-specific additions: - RESTful compliance: Ensures proper HTTP semantics - Security-first: Always validates authentication and authorization - Consistent patterns: Follows project conventions for DTOs and error responses - Testing-focused: Encourages test coverage for endpoints - Performance-aware: Considers query optimization and caching