DEV Community

Cover image for Building Your First Web API with ASP.NET Core Part 4: Putting It All Together & Beyond
Ahsan Khan
Ahsan Khan

Posted on

Building Your First Web API with ASP.NET Core Part 4: Putting It All Together & Beyond

This is the final part of a 4-part series. In Part 3, we completed the write operations POST, PUT, and DELETE. Now we take a step back, look at the big picture, and talk about where to go from here.


What We Built

Over the course of this series, we went from zero to a fully functional RESTful API. Let's take stock of everything that's now in place.

Project Structure

ContosoPizza/
├── Controllers/
│   └── PizzaController.cs     ← handles all HTTP requests
├── Models/
│   └── Pizza.cs               ← our data shape
├── Services/
│   └── PizzaService.cs        ← in-memory data layer
├── Program.cs                 ← app startup & pipeline
├── ContosoPizza.http          ← request testing file
└── ContosoPizza.csproj        ← project config
Enter fullscreen mode Exit fullscreen mode

The Full API Surface

Method Route What It Does Response
GET /pizza Returns all pizzas 200 OK
GET /pizza/{id} Returns one pizza by ID 200 / 404
POST /pizza Adds a new pizza 201 Created
PUT /pizza/{id} Replaces an existing pizza 204 / 400 / 404
DELETE /pizza/{id} Removes a pizza 204 / 404

Five endpoints. Full CRUD coverage. Clean status codes on every path including the unhappy ones.


How the Pieces Fit Together

It's worth zooming out and seeing how data flows through the app from a single request.

Take a POST /pizza request as an example:

  1. The request arrives at the ASP.NET Core HTTP pipeline
  2. The router matches /pizza to PizzaController, and the POST verb to the Create method
  3. [ApiController] deserializes the JSON body into a Pizza object automatically
  4. Create passes that object to PizzaService.Add()
  5. The service assigns an ID and appends it to the in-memory list
  6. The controller returns CreatedAtAction(...), which ASP.NET Core converts into a 201 Created response with a Location header

Every request follows this same arc: route → controller action → service → response. The controller stays thin, the service handles the logic, and the model defines the shape. That separation is what makes the code easy to extend later.


The Limitations of In-Memory Storage

Our PizzaService uses a static List<Pizza> which was perfect for learning, but has real constraints:

  • Data resets on every restart. Stop the server and all your pizzas are gone, back to the two defaults.
  • No concurrency safety. If two requests hit Add() at exactly the same time, you could get race conditions.
  • No persistence. Nothing is written to disk, a file, or a database.

In a real application, you'd swap the in-memory list for a proper database. The good news is that ASP.NET Core is designed for exactly this transition.


Moving to a Real Database

When you're ready to add persistence, Entity Framework Core (EF Core) is the natural next step. It's Microsoft's official ORM for .NET and integrates tightly with ASP.NET Core.

The migration path is straightforward:

1. Install EF Core:

dotnet add package Microsoft.EntityFrameworkCore.SqlServer
# or for SQLite (great for local dev):
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
Enter fullscreen mode Exit fullscreen mode

2. Create a DbContext:

using Microsoft.EntityFrameworkCore;
using ContosoPizza.Models;

public class PizzaContext : DbContext
{
    public PizzaContext(DbContextOptions<PizzaContext> options)
        : base(options) { }

    public DbSet<Pizza> Pizzas => Set<Pizza>();
}
Enter fullscreen mode Exit fullscreen mode

3. Register it in Program.cs:

builder.Services.AddDbContext<PizzaContext>(options =>
    options.UseSqlite("Data Source=contosopizza.db"));
Enter fullscreen mode Exit fullscreen mode

4. Inject it into your controller instead of calling the static service:

public class PizzaController : ControllerBase
{
    private readonly PizzaContext _context;

    public PizzaController(PizzaContext context)
    {
        _context = context;
    }

    [HttpGet]
    public ActionResult<List<Pizza>> GetAll() =>
        _context.Pizzas.ToList();
}
Enter fullscreen mode Exit fullscreen mode

Your routes, attributes, and HTTP behavior stay exactly the same only the data layer changes. That's the benefit of keeping the controller and the service layer separate from the start.


What Else Should You Add?

A working CRUD API is a solid foundation. Here are the most common things you'd layer on next in a real project:

Input Validation

Right now, someone could POST a pizza with no name at all and it would go straight into the list. Data annotations let you enforce rules at the model level:

using System.ComponentModel.DataAnnotations;

public class Pizza
{
    public int Id { get; set; }

    [Required]
    [StringLength(100, MinimumLength = 1)]
    public string? Name { get; set; }

    public bool IsGlutenFree { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Because [ApiController] is on your controller, invalid requests are automatically rejected with a 400 Bad Request no extra code needed.

Swagger / OpenAPI

ASP.NET Core projects come with Swashbuckle pre-installed. It generates an interactive API documentation page automatically from your controller code. Just navigate to /swagger while your app is running and you'll see every endpoint, its expected inputs, and possible responses all explorable in the browser.

Authentication & Authorization

When you're ready to lock down your endpoints, ASP.NET Core has built-in support for JWT Bearer tokens. Add the NuGet package, register the middleware, and protect specific endpoints with [Authorize]:

[Authorize]
[HttpDelete("{id}")]
public IActionResult Delete(int id) { ... }
Enter fullscreen mode Exit fullscreen mode

Async All the Way

Our current service methods are synchronous. In production, especially with a real database, you'd want everything to be async/await to avoid blocking threads under load:

[HttpGet]
public async Task<ActionResult<List<Pizza>>> GetAll() =>
    await _context.Pizzas.ToListAsync();
Enter fullscreen mode Exit fullscreen mode

Series Recap

Here's everything we covered across all four parts:

Part 1 What REST is, why ASP.NET Core, scaffolding the project, running it for the first time, and testing with .http files and HTTP REPL.

Part 2 How controllers, ControllerBase, [ApiController], and [Route] work; building the Pizza model and PizzaService; implementing GET endpoints.

Part 3 Implementing POST, PUT, and DELETE; understanding IActionResult vs ActionResult<T>; correct status codes on every path; end-to-end testing of all five operations.

Part 4 (this one) The full picture, in-memory storage limitations, migrating to EF Core, and the natural next steps for a production-ready API.


Final Thoughts

Building a Web API with ASP.NET Core is remarkably approachable once you understand the underlying structure. Controllers handle routing and HTTP concerns. Services handle logic. Models define the data. Each layer has a clear job.

The patterns you've learned here attribute routing, ActionResult types, proper HTTP status codes, thin controllers, separated service layers are the same ones used in large-scale production APIs. You're not learning toy concepts; you're learning the real thing.

From here, the best thing you can do is keep building. Add a second resource. Connect a real database. Explore authentication. Every layer you add will reinforce what you already know.

Good luck and thanks for following along through the whole series. 🍕


Found this series helpful? Drop a reaction or share it with someone learning .NET. Questions are always welcome in the comments!

Tags: dotnet csharp webapi aspnetcore beginners

Top comments (0)