9. Creating Data (POST)

  1. 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"]
#   }
# }