ASP.NET Core Web API · REST Foundations

REST Principles

REST is a set of design rules that make APIs predictable: everything is a resource with a URL, HTTP methods mean the same thing everywhere, each request stands on its own, and the same resource can be represented in different formats. Let’s see those rules in real responses.

Beginner 7 min Lesson 2 of 37Examples target .NET 9.
Before you start:Introduction to Web APIs
0/37 in ASP.NET Core Web API
On this page · 7 min · Beginner

In simple words#

REST is a common etiquette for APIs: things have addresses (resources), and you use standard verbs — GET to read, POST to create — like everyone following the same traffic rules.

Resource
A thing with a URL: /api/products/5.
Stateless
Each request carries everything needed.
Uniform interface
Standard methods and status codes.

Learning objectives#

  • Model an API around resources and URIs
  • Use content negotiation to return different representations
  • Explain statelessness and why it helps scaling
  • Point clients to new resources with the Location header

Explanation#

Resources are the nouns of your API: products, orders, customers. Each has a stable URI: /api/products (collection) and /api/products/7 (one item). Relationships nest one level: /api/orders/42/lines.

Uniform interface: methods have fixed meanings — GET reads, POST creates, PUT replaces, PATCH updates part, DELETE removes — and status codes describe the outcome. Clients and tools (caches, proxies, SDK generators) rely on that.

Representations: the same resource can be sent as JSON or XML. The client asks with the Accept header; the server answers with Content-Type. With ReturnHttpNotAcceptable = true, ASP.NET Core returns 406 Not Acceptable for formats it cannot produce.

Stateless: each request carries everything needed (for example a token in Authorization). The server keeps no conversation state between requests, so any instance behind a load balancer can answer.

Hypermedia: responses point to related resources. At minimum, a 201 Created response includes a Location header with the new resource’s URI.

Syntax#

Text Illustrative
GET  /api/products/1   Accept: application/json  → 200 Content-Type: application/json
GET  /api/products/1   Accept: application/xml   → 200 Content-Type: application/xml
GET  /api/products/1   Accept: text/csv          → 406 Not Acceptable
POST /api/products                               → 201 Location: /api/products/3

Code example: Representations, 406 and Location#

C#Program.cs Illustrative
// Program.cs
using System.Net.Http.Headers;
using System.Net.Http.Json;


var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();                                  // keep the demo output clean
builder.Services.AddControllers(options => options.ReturnHttpNotAcceptable = true)   // 406 for unknown formats
    .AddXmlSerializerFormatters();                                 // XML as a second representation
builder.Services.AddRouting(options => options.LowercaseUrls = true);   // generate /api/products, not /api/Products


var app = builder.Build();
app.MapControllers();
app.Urls.Add("http://127.0.0.1:5202");
await app.StartAsync();


using var http = new HttpClient { BaseAddress = new Uri("http://127.0.0.1:5202") };
foreach (var accept in new[] { "application/json", "application/xml", "text/csv" })
{
    var request = new HttpRequestMessage(HttpMethod.Get, "/api/products/1");
    request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(accept));
    var response = await http.SendAsync(request);
    Console.WriteLine(
quot;Accept:
{accept,-17} -> {(int)response.StatusCode} {await response.Content.ReadAsStringAsync()}".TrimEnd());
} var created = await http.PostAsJsonAsync("/api/products", new { name = "Laptop Stand", price = 1299 }); Console.WriteLine(
quot;POST /api/products ->
{(int)created.StatusCode}, Location: {created.Headers.Location}");
await app.StopAsync(); // Controllers/ProductsController.cs using Microsoft.AspNetCore.Mvc; public class ProductDto // a class: XmlSerializer needs a parameterless constructor { public int Id { get; set; } public string Name { get; set; } = ""; public decimal Price { get; set; } } [ApiController] [Route("api/[controller]")] public class ProductsController : ControllerBase { private static readonly List<ProductDto> Products = [ new() { Id = 1, Name = "Wireless Mouse", Price = 799m }, new() { Id = 2, Name = "USB-C Hub", Price = 1499m }, ]; [HttpGet("{id:int}")] public ActionResult<ProductDto> Get(int id) => Products.FirstOrDefault(p => p.Id == id) is { } product ? product : NotFound(); [HttpPost] public ActionResult<ProductDto> Create(ProductDto input) { var product = new ProductDto { Id = Products.Max(p => p.Id) + 1, Name = input.Name, Price = input.Price }; Products.Add(product); return CreatedAtAction(nameof(Get), new { id = product.Id }, product); // 201 + Location } }
Expected output
Accept: application/json  -> 200 {"id":1,"name":"Wireless Mouse","price":799}
Accept: application/xml   -> 200 <ProductDto xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"><Id>1</Id><Name>Wireless Mouse</Name><Price>799</Price></ProductDto>
Accept: text/csv          -> 406
POST /api/products -> 201, Location: http://127.0.0.1:5202/api/products/3

This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.

Step-by-step explanation#

  1. The same resource, /api/products/1, is returned as JSON or XML depending on the Accept header.
  2. text/csv cannot be produced, and ReturnHttpNotAcceptable makes the API say so with 406 instead of silently sending JSON.
  3. POST creates a new resource and CreatedAtAction builds its URI from the Get route.
  4. The Location header tells the client exactly where the new product lives. LowercaseUrls makes generated links use /api/products instead of the controller’s casing (/api/Products).
  5. No session or server-side state is used: every request stands alone.

Real-world use case#

A logistics partner integrates with the ShopSmart API using XML while the React app uses JSON — the same controllers serve both, and both clients follow Location headers after creating orders.

Common mistakes#

Verbs in URLs

/api/getProducts or /api/deleteProduct?id=7 break the uniform interface. Use nouns and HTTP methods.

Server-side session state in APIs

Storing “current page” or a cart in server memory breaks scaling and makes requests depend on each other.

Ignoring Accept

Always answering JSON regardless of Accept surprises clients. Return 406 or support the format.

Best practices#

  • Nouns, plural, lowercase URIs
  • Standard methods and status codes
  • Location header on 201
  • Stateless requests with tokens

Remember this#

Resource = noun with a URI. Method = verb.

Accept → representation; unsupported → 406.

Stateless: every request carries what it needs.

Practice exercise#

Your task: 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.

Need a hint?
Nest lines under their order: /api/orders/{orderId}/lines/{lineId}.
Show solution
Text Illustrative
GET    /api/orders/42/lines        200 [ … ]
GET    /api/orders/42/lines/3      200 { … } | 404
POST   /api/orders/42/lines        201 Location: /api/orders/42/lines/4
DELETE /api/orders/42/lines/3      204 | 404

Mini quiz#

REST Principles quizMultiple choice Question 1 of 3

Which header does a client use to ask for XML instead of JSON?

Score so far: 0/0

Want more? Practise all ASP.NET Core Web API MCQs · ASP.NET Core Web API interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.