ASP.NET Core Web API Tutorial
Modern REST APIs with controllers, EF Core, JWT, OpenAPI, versioning and rate limiting.
- Topics
- 37
- Full lessons
- 37
- Est. hours
- ~9
- Interview Qs
- 27
Overview
ASP.NET Core Web API is the standard way to build HTTP APIs on modern .NET. You can use controllers or minimal APIs, both backed by the same middleware pipeline and dependency injection.
This track builds a complete, secure, documented CRUD API and connects it to React and Angular front ends.
What you will learn
- Design RESTful endpoints with correct verbs and status codes
- Validate input with DTOs and data annotations
- Persist data with EF Core or ADO.NET
- Secure APIs with JWT, CORS and rate limiting
Prerequisites
You will get the most from this track if you already know:
Learning path
The recommended sequence, from beginner to advanced. Each stage builds on the one before.
- 1REST Foundations · 4 topics · BeginnerIntroduction to Web APIs · REST Principles · HTTP Methods · HTTP Status Codes
- 2Building APIs · 7 topics · Beginner → IntermediateControllers and Endpoints · Routing · Request and Response Models · DTOs · Model Validation · Dependency Injection · Service Layer
- 3Data · 8 topics · IntermediateADO.NET Integration · Entity Framework Core · SQL Server · Stored Procedures · CRUD API · Pagination · Filtering and Sorting · Searching
- 4Cross-Cutting · 9 topics · Intermediate → AdvancedGlobal Exception Handling · Logging · Swagger and OpenAPI · JWT Authentication · Role-Based Authorization · CORS · API Versioning · Rate Limiting · File Upload APIs
- 5Quality & Delivery · 9 topics · AdvancedAPI Testing · Unit Testing · Integration Testing · Performance · Secure API Design · Docker · Azure Deployment · React + ASP.NET Core API Project · Angular + ASP.NET Core API Project
Complete topic list
37 topicsREST Foundations
4 topics- Introduction to Web APIsBeginner6m
- REST PrinciplesBeginner7m
- HTTP MethodsBeginner8m
- HTTP Status CodesBeginner7m
Building APIs
7 topics- Controllers and EndpointsBeginner6m
- RoutingBeginner6m
- Request and Response ModelsBeginner7m
- DTOsBeginner8m
- Model ValidationBeginner7m
- Dependency InjectionIntermediate6m
- Service LayerIntermediate7m
Data
8 topics- ADO.NET IntegrationIntermediate9m
- Entity Framework CoreIntermediate8m
- SQL ServerIntermediate7m
- Stored ProceduresIntermediate8m
- CRUD APIIntermediate7m
- PaginationIntermediate8m
- Filtering and SortingIntermediate7m
- SearchingIntermediate7m
Cross-Cutting
9 topics- Global Exception HandlingIntermediate7m
- LoggingIntermediate6m
- Swagger and OpenAPIIntermediate6m
- JWT AuthenticationIntermediate7m
- Role-Based AuthorizationIntermediate7m
- CORSIntermediate6m
- API VersioningIntermediate6m
- Rate LimitingAdvanced7m
- File Upload APIsAdvanced7m
Quality & Delivery
9 topics- API TestingAdvanced7m
- Unit TestingAdvanced7m
- Integration TestingAdvanced7m
- PerformanceAdvanced7m
- Secure API DesignAdvanced8m
- DockerAdvanced5m
- Azure DeploymentAdvanced6m
- React + ASP.NET Core API ProjectAdvanced8m
- Angular + ASP.NET Core API ProjectAdvanced8m
Code example
A taste of the track, from Introduction to Web APIs.
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders(); // keep the demo output clean
builder.Services.AddControllers(); // controller-based APIs
var app = builder.Build();
app.MapControllers();
app.MapGet("/api/status", () => new { status = "ok", version = "1.0" }); // a minimal API endpoint
app.Urls.Add("http://127.0.0.1:5201");
await app.StartAsync();
using var http = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:5201") };
foreach (var path in new[] { "/api/products", "/api/products/2", "/api/products/9", "/api/status" })
{
var response = await http.GetAsync(path);
var type = response.Content.Headers.ContentType?.MediaType ?? "-";
Console.WriteLine(quot;GET {path,-16} {(int)response.StatusCode} {type,-25} {await response.Content.ReadAsStringAsync()}");
}
await app.StopAsync();
// Controllers/ProductsController.cs
using Microsoft.AspNetCore.Mvc;
public record ProductDto(int Id, string Name, decimal Price);
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
private static readonly ProductDto[] Products = [new(1, "Wireless Mouse", 799m), new(2, "USB-C Hub", 1499m)];
[HttpGet]
public IEnumerable<ProductDto> GetAll() => Products;
[HttpGet("{id:int}")]
public ActionResult<ProductDto> Get(int id) =>
Products.FirstOrDefault(p => p.Id == id) is { } product ? product : NotFound();
}
GET /api/products 200 application/json [{"id":1,"name":"Wireless Mouse","price":799},{"id":2,"name":"USB-C Hub","price":1499}]
GET /api/products/2 200 application/json {"id":2,"name":"USB-C Hub","price":1499}
GET /api/products/9 404 application/problem+json {"type":"https://tools.ietf.org/html/rfc9110#section-15.5.5","title":"Not Found","status":404,"traceId":"0HNPL7JNPL4ED:00000003"}
GET /api/status 200 application/json {"status":"ok","version":"1.0"}
(the traceId varies per request)Practice exercises
Add a minimal API endpoint GET /api/time that returns { utc: DateTime.UtcNow } and a controller action GET /api/products/count that returns the number of products.
Design the URIs for order lines: list the lines of order 42, get line 3 of order 42, add a line to order 42, remove line 3.
Add HEAD support for GET /api/products/{id} by also marking the action with [HttpHead("{id:int}")], so clients can check existence without downloading the body.
Add an action POST /api/orders/{id}/ship that returns 404 for unknown orders, 409 if the order was already shipped, and 202 Accepted otherwise (shipping happens later).
Move the minimal API endpoints into a static extension method MapProductsV2(this IEndpointRouteBuilder app) in its own file, and call it from Program.cs.
Add GET /api/products/search/{term:minlength(2)} returning product names containing the term (case-insensitive).
Show all 37 exercises
Make the API accept and return snake_case JSON (customer_id, gift_message) instead of camelCase.
Create an UpdatePriceDto (only Price, between 1 and 1,000,000) and a PATCH /api/products/{id}/price action that changes only the price.
Customise the automatic 400 response so it also includes an "errorCount" extension with the number of invalid fields.
Inject the express calculator directly into an action with [FromKeyedServices("express")] and return its cost for 1 kg.
Add an ErrorType.Forbidden mapped to 403 and a service rule: products with id over 100 are “staff only” and cannot be reserved by customers.
Add GET /api/orders/{id} that returns { id, email, total } using ExecuteReaderAsync, or 404 when the order does not exist.
Add DELETE /api/products/discontinued that deletes every product with Stock == 0 using ExecuteDeleteAsync and returns { deleted }.
Make the health endpoint return JSON like {"status":"Healthy","checks":{"database":"Healthy"}} using HealthCheckOptions.ResponseWriter.
Call usp_GetProductsByCategory with plain ADO.NET (CommandType.StoredProcedure) and return the product names.
Add GET /api/products/search?name=key that returns products whose name contains the search text.
Add an X-Total-Count response header to the offset endpoint so clients that ignore the body can read the total.
Allow sorting by "stock" too, and add a filter ?names=Mouse Pad&names=USB-C Hub that returns only those products.
Change the search so every word must match: q="usb hub" returns products whose name or description contains both “usb” and “hub”.
Make the handler map OperationCanceledException (client disconnected) to status 499 without logging it as an error, and return true without writing a body.
Wrap the order-placing code in a log scope with OrderId so every log line inside it carries that field, and enable IncludeScopes on the JSON console formatter.
Add the Scalar interactive UI in Development only, available at /scalar/v1.
Add a [Authorize] GET /api/orders/mine endpoint that returns the caller’s user id from the sub claim, and returns 401 for anonymous callers.
Add a policy "ReadReports" that requires the claim scope=reports:read, and use it on GET /api/reports.
Read the allowed origins from configuration (Cors:Origins array) instead of hard-coding them.
Add a v2-only endpoint GET /api/v2/products/{id}/reviews that does not exist in v1.
Add a fixed-window policy "login" that allows 5 requests per minute per IP address and apply it to POST /api/auth/login.
Allow uploading several images at once with IFormFileCollection and return the count of accepted files (skip non-JPEGs).
Write curl commands that create a product, print the response headers, and then delete the product using its id.
Add a Theory row proving that a gold customer buying 1 item at ₹999.99 pays ₹849.99 (rounded to 2 decimals).
Add a test that posts a product with a 101-character name and asserts a 400 response with an error for Name.
Add a "Product" output-cache policy that varies by the id route value and expires after 2 minutes, and use it on GET /api/products/{id}.
Let staff (role Support) read any order while customers can still only read their own. Return 404 for customers reading other orders.
Write a docker-compose.yml that starts SQL Server and the API, waits for SQL Server to be healthy, and maps the API to port 5080.
Write a GitHub Actions workflow that builds the API on push to main and deploys it to the staging slot with azure/webapps-deploy.
Add a deleteProduct(id) function to api.ts and a Delete button per product that removes it from the list after a 204.
Add a delete(id) method to ProductService and a Delete button in the list that removes the product from the products signal after success.
Common interview questions
- Why use DTOs instead of returning EF entities?
- Which status codes should CRUD endpoints return?
- What does the [ApiController] attribute do?
- What makes an HTTP method idempotent?
- What is the difference between ControllerBase and Controller?
- What does ActionResult<T> give you?
- What is OpenAPI and how do you add it in .NET 9?
- What is CORS and why does my React app get a CORS error?
- Controllers vs minimal APIs?
- How do you implement global exception handling?
- How does JWT bearer authentication work in ASP.NET Core?
- How do you version an API?
- How do you use Entity Framework Core efficiently in an API?
- When would you use Dapper or ADO.NET instead of EF Core?
- How do you return validation errors in a standard format?
- How do you implement search with filtering and paging?
- What is ProblemDetails?
- Role-based vs policy-based authorization?
- How do you test an API with .http files or Postman?
- How does built-in rate limiting work?
- Unit tests vs integration tests for an API?
- How do you improve API performance?
- How do you handle file uploads in an API?
- How do you secure an ASP.NET Core API end to end?
- How do you run an API in Docker?
- How do you deploy an API to Azure?
- How do you connect a React or Angular front end to the API?