POST

POST creates a new item from the JSON in the request body. The DAO inserts it and returns it with its new id, and the action answers 201 Created with CreatedAtAction, which also says where the new item lives. Invalid bodies get 400 automatically.

Journeyman · ASP.NET Core · REST API · Updated

After this lesson you can

  • Write a POST action that adds an item from the body and answers 201 Created with CreatedAtAction
  • Explain how [ApiController] answers 400 Bad Request by itself when the body breaks a validation rule

Key takeaways

  • A POST to the collection's address creates an item. In an [ApiController], a parameter of a class type comes from the JSON body by itself.
  • OUTPUT INSERTED.CategoryId hands back the new id from the INSERT, and Dapper's ExecuteScalar<int> reads it.
  • CreatedAtAction(nameof(GetById), new { id = added.CategoryId }, added) answers 201 Created with the new item and a Location header.
  • When the body breaks a validation rule, [ApiController] answers 400 Bad Request itself, and your action never runs.
  • That 400 is a problem details response (application/problem+json): a standard JSON shape that lists the errors by field.

A client creates a new item with a POST to the collection's address, sending the new item as JSON in the body. The server saves it and answers with the saved item, now with its new id.

The DAO: insert, and get the new id

The database chooses the new CategoryId (it's an identity column). SQL Server's OUTPUT INSERTED.CategoryId hands it back from the INSERT itself, and Dapper's ExecuteScalar reads it:

public Category Add(Category category)
{
    string sql = """
        INSERT INTO Categories (CategoryName, Description)
        OUTPUT INSERTED.CategoryId
        VALUES (@CategoryName, @Description);
        """;

    using SqlConnection connection = new SqlConnection(connectionString);
    category.CategoryId = connection.ExecuteScalar<int>(sql, category);
    return category;
}

Passing category itself as the parameters works because its property names match @CategoryName and @Description.

The action

[HttpPost]
public ActionResult<Category> Add(Category category)
{
    Category added = categoryDao.Add(category);
    return CreatedAtAction(nameof(GetById), new { id = added.CategoryId }, added);
}
  • [HttpPost] answers POST /api/categories.
  • Category category is filled from the JSON body. In an [ApiController], a parameter of a class type comes from the body without being told. (You can write [FromBody] Category category to make it explicit.)
  • ActionResult<Category> means "either a Category, or a status code result". Returning ActionResult<T> covers it in full; here it lets the action return CreatedAtAction(...).
  • CreatedAtAction answers 201 Created, puts the new category in the body, and adds a Location header with the new category's address. It builds that address from the GetById action and the route values new { id = ... }, so it's always right, even if the route changes.
POST {{HostAddress}}/api/categories
Content-Type: application/json

{
  "categoryName": "Snacks",
  "description": "Chips, nuts and pretzels"
}
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Location: http://localhost:5179/api/categories/9

{
  "categoryId": 9,
  "categoryName": "Snacks",
  "description": "Chips, nuts and pretzels"
}

The client now knows the new id, and the exact address to GET it from.

Bad bodies get 400 for free

Category has [Required] and [StringLength(15)] on CategoryName. Send a body that breaks them:

POST {{HostAddress}}/api/categories
Content-Type: application/json

{
  "categoryName": "A name that is far too long",
  "description": "Too long"
}
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json; charset=utf-8

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
  "title": "One or more validation errors occurred.",
  "status": 400,
  "errors": {
    "CategoryName": [
      "The field CategoryName must be a string with a maximum length of 15."
    ]
  }
}

Your action never ran. [ApiController] checked the annotations, found an error, and answered 400 Bad Request itself, listing every problem by field. In MVC you checked ModelState.IsValid and showed the form again; in an API, the client gets this list and shows it however it likes.

That body is a problem details response (application/problem+json): a standard JSON shape for errors, with a title, a status and, here, the errors. Clients everywhere know how to read it. (The response also carries a traceId, an id for finding this request in the server's logs.)

Check yourself

2 questions to try on the lesson page.

So what

POST is insert-and-tell: save the item, then answer 201 Created with the item and its address. Put validation rules on the model, and [ApiController] turns away bad requests before they reach your code.

Key terms

Cheat sheets