9. Creating Data (POST)
- Creating Data (POST)
About this chapter
Build POST endpoints that safely create new resources, validate input, and return proper HTTP responses with location headers.
- POST endpoints: Creating resources with request body binding
- Model binding: Automatic JSON deserialization with [ApiController]
- HTTP 201 Created: Returning new resource with Location header
- CreatedAtRoute: Linking to the newly created resource
- User context: Capturing current user in audit fields
- Input validation: Ensuring data integrity before persistence
Learning outcomes:
- Create POST endpoints with proper model binding
- Return 201 Created with Location header using CreatedAtRoute
- Capture and store user information (UserId, timestamps)
- Validate incoming data before database operations
- Map DTOs to domain models for persistence
- Handle creation scenarios and edge cases
9.1 POST Endpoints and Model Binding
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created, Type = typeof(PlatformReadDto))]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<PlatformReadDto>> CreatePlatform(
PlatformMutateDto platformDto)
{
// platformDto automatically bound from request body (JSON)
var platformModel = _mapper.Map<Platform>(platformDto);
platformModel.CreatedBy = GetCurrentUserId();
await _repository.CreatePlatformAsync(platformModel);
await _repository.SaveChangesAsync();
var platformReadDto = _mapper.Map<PlatformReadDto>(platformModel);
return CreatedAtRoute(
nameof(GetPlatformById),
new { id = platformReadDto.Id },
platformReadDto);
}
- Model Binding: [FromBody] inferred by [ApiController]
- 201 Created: Includes Location header pointing to new resource
9.2 CORRECTION: Proper UserId Handling
// ❌ WRONG (current code)
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "undefined";
// ✅ BETTER
private string GetCurrentUserId()
{
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
throw new UnauthorizedAccessException("User ID claim not found");
}
return userId;
}
// ✅ OR return BadRequest
var userId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(userId))
{
return BadRequest(new { message = "User authentication required" });
}
- Why This Matters: “undefined” string pollutes database
- Alternative: Make CreatedBy nullable and allow null
9.3 CreatedAtRoute Pattern
// Returns:
// Status: 201 Created
// Location: https://api.example.com/api/platforms/5
// // Body: { "id": 5, "platformName": "Kubernetes" }
return CreatedAtRoute(
routeName: nameof(GetPlatformById),
routeValues: new { id = platformReadDto.Id },
value: platformReadDto
);
- REST Best Practice: Return created resource with location
- Client Benefit: Can immediately fetch full resource if needed
9.4 Model Validation and ModelState
[HttpPost]
public async Task<ActionResult<PlatformReadDto>> CreatePlatform(
PlatformMutateDto platformDto)
{
// [ApiController] attribute automatically validates and returns 400
// Manual check only needed for custom validation:
if (await _repository.PlatformNameExistsAsync(platformDto.PlatformName))
{
ModelState.AddModelError(
nameof(platformDto.PlatformName),
"Platform name already exists");
return BadRequest(ModelState);
}
// Continue with creation...
}
- Automatic Validation: [ApiController] checks data annotations
- ModelState.AddModelError: Add custom validation errors
- Response Format: Standardized validation error JSON
9.5 Testing POST with HTTP Files
### Create Platform
POST {{baseUrl}}/api/platforms
Content-Type: application/json
x-api-key: {{apiKey}}
{
"platformName": "Kubernetes"
}
### Expected Response
# HTTP/1.1 201 Created
# Location: https://localhost:7213/api/platforms/5
# {
# "id": 5,
# "platformName": "Kubernetes",
# "commandCount": 0
# }
### Test Validation Error
POST {{baseUrl}}/api/platforms
Content-Type: application/json
x-api-key: {{apiKey}}
{
"platformName": ""
}
### Expected Response
# HTTP/1.1 400 Bad Request
# {
# "type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",# "title": "One or more validation errors occurred.",
# "status": 400,
# "errors": {
# "PlatformName": ["Platform name is required"]
# }
# }