
Learn how to build a REST API with ASP.NET Core 8 step by step with runnable C# code, EF Core, CRUD endpoints, and best practices. Start building today!
If you want to build a REST API with ASP.NET Core, this step-by-step tutorial walks you through everything you need to ship a working, production-ready Web API in .NET 8. By the end of this guide you'll have a full CRUD REST API in C# backed by Entity Framework Core, complete with routing, dependency injection, validation, and Swagger documentation. This ASP.NET Core 8 Web API tutorial is written for beginners who are searching "how to create a REST API" as well as intermediate developers who want best practices they can drop into real projects.
What Is a REST API and Why ASP.NET Core 8?
A REST API (Representational State Transfer) exposes resources over HTTP using predictable verbs: GET to read, POST to create, PUT to update, and DELETE to remove. ASP.NET Core 8 is Microsoft's high-performance, cross-platform framework for building these APIs. It runs on Windows, Linux, and macOS, ships with built-in dependency injection, and consistently ranks among the fastest web frameworks in independent benchmarks.
Developers choose ASP.NET Core 8 to build a REST API with ASP.NET Core because of long-term support (LTS), first-class tooling in Visual Studio and the .NET CLI, and seamless integration with Entity Framework Core for database access. Whether you're in the USA, UK, Canada, Australia, or India, these skills are in high demand for backend and full-stack roles.
Prerequisites
- The .NET 8 SDK installed (run
dotnet --versionto confirm 8.x). - Visual Studio 2022, VS Code, or JetBrains Rider.
- Basic C# knowledge — classes, methods, and async/await.
Step 1: Create the ASP.NET Core 8 Web API Project
Open a terminal and scaffold a new Web API. Using the .NET CLI keeps the steps identical across every operating system:
dotnet new webapi -n BookStoreApi --use-controllers
cd BookStoreApi
dotnet run
The --use-controllers flag gives you the controller-based template (rather than Minimal APIs), which is ideal when you want clear separation and scalability. When the app starts, browse to the HTTPS URL shown in the console and append /swagger to see the auto-generated API documentation.
Step 2: Define Your Model
Every REST API revolves around resources. Create a Models folder and add a Book.cs class. Data annotations give you free validation later.
using System.ComponentModel.DataAnnotations;
namespace BookStoreApi.Models;
public class Book
{
public int Id { get; set; }
[Required]
[StringLength(200)]
public string Title { get; set; } = string.Empty;
[Required]
[StringLength(100)]
public string Author { get; set; } = string.Empty;
[Range(0, 10000)]
public decimal Price { get; set; }
public DateTime PublishedOn { get; set; }
}
Step 3: Add Entity Framework Core for Data Access
To persist data we'll use Entity Framework Core, the most common ORM for an ASP.NET Core Web API. Add the packages (we use the lightweight in-memory provider here so you can run this tutorial with zero database setup; swap it for SQL Server in production):
dotnet add package Microsoft.EntityFrameworkCore.InMemory
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
Create a Data folder with an AppDbContext. The DbSet<Book> becomes your queryable table.
using BookStoreApi.Models;
using Microsoft.EntityFrameworkCore;
namespace BookStoreApi.Data;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options)
: base(options) { }
public DbSet<Book> Books => Set<Book>();
}
Register the context with the built-in dependency injection container in Program.cs. This is why ASP.NET Core scales cleanly — services are configured once and injected everywhere.
using BookStoreApi.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseInMemoryDatabase("BookStore"));
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Step 4: Build the CRUD Controller
Now for the heart of the tutorial: a full CRUD API controller. Create Controllers/BooksController.cs. Each action maps to an HTTP verb and returns the correct status code — a hallmark of a well-designed REST API.
using BookStoreApi.Data;
using BookStoreApi.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
namespace BookStoreApi.Controllers;
[ApiController]
[Route("api/[controller]")]
public class BooksController : ControllerBase
{
private readonly AppDbContext _context;
public BooksController(AppDbContext context)
{
_context = context;
}
// GET: api/books
[HttpGet]
public async Task<ActionResult<IEnumerable<Book>>> GetBooks()
{
return await _context.Books.AsNoTracking().ToListAsync();
}
// GET: api/books/5
[HttpGet("{id:int}")]
public async Task<ActionResult<Book>> GetBook(int id)
{
var book = await _context.Books.FindAsync(id);
return book is null ? NotFound() : book;
}
// POST: api/books
[HttpPost]
public async Task<ActionResult<Book>> CreateBook(Book book)
{
_context.Books.Add(book);
await _context.SaveChangesAsync();
return CreatedAtAction(nameof(GetBook), new { id = book.Id }, book);
}
// PUT: api/books/5
[HttpPut("{id:int}")]
public async Task<IActionResult> UpdateBook(int id, Book book)
{
if (id != book.Id) return BadRequest("Route id and body id must match.");
_context.Entry(book).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException) when (!BookExists(id))
{
return NotFound();
}
return NoContent();
}
// DELETE: api/books/5
[HttpDelete("{id:int}")]
public async Task<IActionResult> DeleteBook(int id)
{
var book = await _context.Books.FindAsync(id);
if (book is null) return NotFound();
_context.Books.Remove(book);
await _context.SaveChangesAsync();
return NoContent();
}
private bool BookExists(int id) => _context.Books.Any(b => b.Id == id);
}
A few things make this controller idiomatic. The [ApiController] attribute enables automatic model validation (no manual ModelState.IsValid checks), automatic 400 responses, and binding-source inference. CreatedAtAction returns a 201 with a Location header pointing at the new resource — exactly what REST clients expect. And AsNoTracking() on read-only queries skips EF Core's change tracker for a real performance win.
Step 5: Test Your REST API
Run dotnet run and open Swagger UI. Try a POST to /api/books with this JSON body:
{
"title": "Clean Code",
"author": "Robert C. Martin",
"price": 39.99,
"publishedOn": "2008-08-01T00:00:00"
}
You can also test from the command line with curl, which works identically on Windows, macOS, and Linux:
curl -X POST https://localhost:5001/api/books \
-H "Content-Type: application/json" \
-d '{"title":"Clean Code","author":"Robert C. Martin","price":39.99,"publishedOn":"2008-08-01"}'
curl https://localhost:5001/api/books
ASP.NET Core Web API Best Practices
Once you can build a REST API with ASP.NET Core, these best practices separate a hobby project from production code:
- Use DTOs, not entities. Returning EF Core entities directly leaks your database schema and risks over-posting attacks. Map to request/response DTOs instead.
- Version your API. Add
api/v1/booksroutes so you can evolve without breaking existing clients. - Return consistent error responses. ASP.NET Core 8's
ProblemDetails(RFC 7807) gives standardized error payloads out of the box. - Page large result sets. Never return thousands of rows; accept
pageandpageSizequery parameters. - Apply async all the way. Use async EF Core methods so threads aren't blocked on I/O — critical for scalability under load.
- Secure it. Add JWT authentication and the
[Authorize]attribute before going to production.
Common Pitfalls to Avoid
- Forgetting status codes. Returning 200 for everything breaks REST clients. Use 201, 204, 400, 404 appropriately.
- Synchronous database calls. Blocking calls like
ToList()instead ofToListAsync()throttle throughput. - Over-posting. Binding directly to entities lets clients set fields they shouldn't — another reason to use DTOs.
- Leaving Swagger enabled in production without protection can expose your full API surface.
Conclusion and Key Takeaways
You've now seen how to build a REST API with ASP.NET Core 8 from an empty folder to a fully working CRUD service. The same pattern — model, DbContext, controller, and tested endpoints — scales from a weekend project to enterprise microservices used by teams across the USA, UK, Canada, Australia, and India.
Key takeaways:
- Scaffold quickly with
dotnet new webapiand document automatically with Swagger. - Use Entity Framework Core and dependency injection for clean, testable data access.
- Map HTTP verbs to controller actions and return correct status codes for a true REST API.
- Apply DTOs, versioning, async, and authentication before shipping to production.
Next, swap the in-memory provider for SQL Server, add JWT auth, and deploy to Azure App Service or Docker. Bookmark csharp-coder.com and keep building — your ASP.NET Core 8 Web API skills are exactly what employers are searching for in 2026.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment