DEV Community

Cover image for Building Your First Web API with ASP.NET Core Part 3: Implementing POST, PUT & DELETE
Ahsan Khan
Ahsan Khan

Posted on

Building Your First Web API with ASP.NET Core Part 3: Implementing POST, PUT & DELETE

This is Part 3 of a 4-part series. In Part 2, we built our Pizza model, an in-memory data service, and wired up two GET endpoints. Now it's time to handle the write side of our API.


Where We Left Off

By the end of Part 2, our PizzaController could respond to two GET requests:

  • GET /pizza returns all pizzas
  • GET /pizza/{id} returns a specific pizza or a 404

That covers the Read in CRUD. This part finishes the job with Create, Update, and Delete using POST, PUT, and DELETE respectively.

Here's the full HTTP verb-to-CRUD mapping we're working with:

HTTP Verb CRUD Operation ASP.NET Core Attribute
GET Read [HttpGet]
POST Create [HttpPost]
PUT Update [HttpPut]
DELETE Delete [HttpDelete]

IActionResult vs ActionResult<T> A Quick Clarification

In Part 2, our GET methods returned ActionResult<T> a typed wrapper that tells ASP.NET Core exactly what shape the response body will have.

For write operations, we'll use IActionResult instead. Why? Because write endpoints often return no body at all just a status code. When you update a pizza successfully, you don't need to send the pizza back; a 204 No Content is enough. Since the return type isn't known until runtime, IActionResult gives us the flexibility to return different result types from the same method.


POST Adding a New Pizza

Replace the // POST action comment in PizzaController.cs with:

[HttpPost]
public IActionResult Create(Pizza pizza)
{
    PizzaService.Add(pizza);
    return CreatedAtAction(nameof(Get), new { id = pizza.Id }, pizza);
}
Enter fullscreen mode Exit fullscreen mode

Let's unpack what's happening here.

The [HttpPost] attribute maps this method to POST /pizza. When the client sends a request with a JSON body, ASP.NET Core automatically deserializes it into a Pizza object no manual parsing needed. This works because the controller has [ApiController] on it, which tells the framework to look for the object in the request body by default.

The return value, CreatedAtAction, does two things at once:

  • Sends back a 201 Created status code
  • Adds a Location header to the response pointing to the newly created resource (e.g., /pizza/3)

That nameof(Get) refers to our single-item GET method from Part 2. It's used to build the URL in the Location header without hardcoding any strings.

Result HTTP Status When
CreatedAtAction(...) 201 Created Pizza was added successfully
Implicit BadRequest 400 Bad Request Request body failed model validation

PUT Updating an Existing Pizza

Replace the // PUT action comment with:

[HttpPut("{id}")]
public IActionResult Update(int id, Pizza pizza)
{
    if (id != pizza.Id)
        return BadRequest();

    var existingPizza = PizzaService.Get(id);
    if (existingPizza is null)
        return NotFound();

    PizzaService.Update(pizza);
    return NoContent();
}
Enter fullscreen mode Exit fullscreen mode

A few deliberate design decisions here worth understanding.

The route is PUT /pizza/{id}, and the method takes both an id from the URL and a Pizza object from the request body. The first thing we do is check that these two IDs agree if someone sends PUT /pizza/1 with a body that has "id": 5, that's a conflicting request and we return 400 Bad Request immediately.

Next, we check whether the pizza actually exists before trying to update it. If it doesn't, 404 Not Found is the correct response not a silent no-op.

On success, we return 204 No Content. There's no need to send the updated pizza back in the response body; the client already has it since they just sent it to us.

Result HTTP Status When
NoContent() 204 No Content Update was applied successfully
BadRequest() 400 Bad Request URL id and body id don't match
NotFound() 404 Not Found No pizza with that ID exists

DELETE Removing a Pizza

Replace the // DELETE action comment with:

[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
    var pizza = PizzaService.Get(id);

    if (pizza is null)
        return NotFound();

    PizzaService.Delete(id);
    return NoContent();
}
Enter fullscreen mode Exit fullscreen mode

This is the most straightforward of the three. The route is DELETE /pizza/{id} no request body needed, just the ID in the URL.

We verify the pizza exists first. If not, 404. If it does, we delete it and return 204 No Content to confirm the operation succeeded without sending anything back.

Result HTTP Status When
NoContent() 204 No Content Pizza was deleted successfully
NotFound() 404 Not Found No pizza with that ID exists

The Complete PizzaController

Here's the full controller at this point all five actions in one place:

using ContosoPizza.Models;
using ContosoPizza.Services;
using Microsoft.AspNetCore.Mvc;

namespace ContosoPizza.Controllers;

[ApiController]
[Route("[controller]")]
public class PizzaController : ControllerBase
{
    public PizzaController() { }

    [HttpGet]
    public ActionResult<List<Pizza>> GetAll() =>
        PizzaService.GetAll();

    [HttpGet("{id}")]
    public ActionResult<Pizza> Get(int id)
    {
        var pizza = PizzaService.Get(id);
        if (pizza == null)
            return NotFound();
        return pizza;
    }

    [HttpPost]
    public IActionResult Create(Pizza pizza)
    {
        PizzaService.Add(pizza);
        return CreatedAtAction(nameof(Get), new { id = pizza.Id }, pizza);
    }

    [HttpPut("{id}")]
    public IActionResult Update(int id, Pizza pizza)
    {
        if (id != pizza.Id)
            return BadRequest();

        var existingPizza = PizzaService.Get(id);
        if (existingPizza is null)
            return NotFound();

        PizzaService.Update(pizza);
        return NoContent();
    }

    [HttpDelete("{id}")]
    public IActionResult Delete(int id)
    {
        var pizza = PizzaService.Get(id);
        if (pizza is null)
            return NotFound();

        PizzaService.Delete(id);
        return NoContent();
    }
}
Enter fullscreen mode Exit fullscreen mode

Clean, readable, and covers all five operations in under 50 lines of code.


Build & Run

Save the file, then verify there are no errors:

dotnet build
Enter fullscreen mode Exit fullscreen mode

Start the server:

dotnet run
Enter fullscreen mode Exit fullscreen mode

Testing the Write Endpoints

Open ContosoPizza.http and add the following test requests:

### Add a new pizza
POST {{ContosoPizza_HostAddress}}/pizza/
Content-Type: application/json

{
  "name": "Hawaii",
  "isGlutenFree": false
}

###

### Update it (fix the name)
PUT {{ContosoPizza_HostAddress}}/pizza/3
Content-Type: application/json

{
  "id": 3,
  "name": "Hawaiian",
  "isGlutenFree": false
}

###

### Confirm the update
GET {{ContosoPizza_HostAddress}}/pizza/3
Accept: application/json

###

### Delete it
DELETE {{ContosoPizza_HostAddress}}/pizza/3

###

### Confirm it's gone
GET {{ContosoPizza_HostAddress}}/pizza/
Accept: application/json

###
Enter fullscreen mode Exit fullscreen mode

Run them in order and here's what you should see:

  • POST201 Created with the new pizza in the body and a Location header
  • PUT204 No Content (no body, just confirmation)
  • GET /pizza/3200 OK showing the updated name "Hawaiian"
  • DELETE204 No Content
  • GET /pizza/200 OK with only the original two pizzas ID 3 is gone

💡 Note: Since we're using in-memory storage, IDs don't persist across server restarts. Every time you run dotnet run, the list resets to the two seed pizzas.


What's Coming in Part 4,?

Our API is now fully functional. In the final part, we'll wrap everything up with:

  • A recap of how all the pieces fit together
  • A look at what you'd change moving from in-memory to a real database
  • Next steps for taking your API further (authentication, validation, Swagger UI)

Only one part left follow along so you don't miss the wrap-up. Questions? Drop them in the comments!

Tags: dotnet csharp webapi aspnetcore beginners

Top comments (0)