Interview question
Build a typed create-resource endpoint
Creates a resource through an explicit request DTO and returns typed 201, 409, and validation outcomes.
TL;DR
Creates a resource through an explicit request DTO and returns typed 201, 409, and validation outcomes.
Request DTOs, typed results, Location headers, conflict handling, cancellation, and response shaping.
Practice the problem like a real interview: restate, reason, implement, and test.
Map POST /projects. Accept only name and slug, reject a duplicate slug, and return 201 Created with a route-based Location header and a response DTO.
Creating billing-api returns 201 and /projects/{id}; creating the same slug again returns a safe 409 problem response.
I keep transport, domain, and persistence shapes separate. The handler validates the public request, checks the domain conflict, saves once, and describes every expected outcome in its result type.
app.MapPost("/projects", async Task<Results<
CreatedAtRoute<ProjectResponse>, ValidationProblem, Conflict<ProblemDetails>>>
(CreateProjectRequest request, AppDbContext db, CancellationToken ct) =>
{
var errors = ProjectValidation.Validate(request);
if (errors.Count > 0)
return TypedResults.ValidationProblem(errors);
var slug = request.Slug.Trim().ToLowerInvariant();
if (await db.Projects.AnyAsync(x => x.Slug == slug, ct))
return TypedResults.Conflict(new ProblemDetails {
Title = "Project slug is already in use",
Status = StatusCodes.Status409Conflict
});
var project = new Project(request.Name.Trim(), slug);
db.Projects.Add(project);
try {
await db.SaveChangesAsync(ct);
} catch (DbUpdateException ex) when (
UniqueConstraints.IsViolation(ex, "UX_Projects_Slug")) {
return TypedResults.Conflict(new ProblemDetails {
Title = "Project slug is already in use",
Status = StatusCodes.Status409Conflict
});
}
return TypedResults.CreatedAtRoute(
new ProjectResponse(project.Id, project.Name, project.Slug),
"GetProject", new { id = project.Id });
})
.WithName("CreateProject");
public sealed record CreateProjectRequest(string Name, string Slug);
public sealed record ProjectResponse(Guid Id, string Name, string Slug);
Typed results keep the endpoint contract visible to the compiler and OpenAPI. A unique database constraint must still protect the slug race after the friendly pre-check.
Move to the linked follow-up, next path step, prerequisite, or deeper variant.
Practice the next layer of the same subject.