← all posts

.NET

What I Learned About Building REST APIs in .NET

A plain CRUD API, no payments, nothing fancy. HTTP verbs, routing, EF Core, and status codes are finally clicking while I build it.

What I Learned About Building REST APIs in .NET

I just finished my first real REST API in .NET: a plain CRUD app, no payments, nothing fancy. It's basic, but building it is where a lot of the fundamentals are finally clicking for me.

Matching HTTP verbs to what they're supposed to do

I knew GET, POST, PUT, and DELETE existed before I started this, but I didn't really get why it mattered which one you used until I built something with all four. GET reads, POST creates, PUT updates, DELETE removes, and each one maps cleanly onto a Controller action. Seeing them side by side, it stops being an abstract rule and starts being obvious.

Routing and Controllers

I'm using Controllers because that's what every tutorial I've followed uses. [ApiController], [Route("api/products")], and an action per HTTP verb:

[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
    private readonly AppDbContext _context;

    public ProductsController(AppDbContext context)
    {
        _context = context;
    }

    [HttpGet]
    public async Task<IActionResult> GetAll()
    {
        var products = await _context.Products.ToListAsync();
        return Ok(products);
    }
}

It felt like a lot of attributes to memorize at first, but once I understood that the route and the verb together decide which action runs, it stops feeling arbitrary.

Talking to the database with EF Core

This is the first time I've used Entity Framework Core for something real instead of a tutorial's toy example. A model, a DbContext, and a connection string:

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
}

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

    public DbSet<Product> Products => Set<Product>();
}

Then running the migration to actually create the table:

dotnet ef migrations add InitialCreate
dotnet ef database update

That's the first time the API felt like it was doing something instead of just returning fake data.

Status codes actually mean something

Before this I would have returned 200 for basically everything. Building this API is the first time I'm paying attention to which one I'm actually sending back:

[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
    var product = await _context.Products.FindAsync(id);

    if (product is null)
    {
        return NotFound();
    }

    return Ok(product);
}

[HttpPost]
public async Task<IActionResult> Create(Product product)
{
    _context.Products.Add(product);
    await _context.SaveChangesAsync();

    return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}

NotFound() gives a 404, Ok() a 200, and CreatedAtAction returns a 201 along with the URL of the thing that was just created. Getting those right makes the API make sense to anyone calling it without reading the code.

Where this leaves me

Nothing about this project is advanced. It's a plain CRUD API with a database behind it, and most of what I've learned is just the basics clicking into place for the first time. That's all this one is right now, and that's enough for where I'm at.