# 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 November 3, 2026

## 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:

```csharp
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

```csharp
[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&lt;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.

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

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

```http
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:

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

{
  "categoryName": "A name that is far too long",
  "description": "Too long"
}
```

```http
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

- [POST](https://www.ask-codey.com/glossary/post)
- CreatedAtAction
- Model Validation
- [Problem Details](https://www.ask-codey.com/glossary/problem-details)

## Cheat sheets

- [POST](https://www.ask-codey.com/cheat-sheets/web-apis-with-controllers-post-cheat-sheet)

Source: https://www.ask-codey.com/lessons/web-apis-with-controllers-post
