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.
On this page · 7 min · Beginner
On this page
- In simple words
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 7 min
- Difficulty
- Beginner
- Updated
- Oct 29, 2026
Related tutorials
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#
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#
// 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
}
}
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/3This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- The same resource, /api/products/1, is returned as JSON or XML depending on the Accept header.
- text/csv cannot be produced, and ReturnHttpNotAcceptable makes the API say so with 406 instead of silently sending JSON.
- POST creates a new resource and CreatedAtAction builds its URI from the Get route.
- 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).
- 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#
/api/getProducts or /api/deleteProduct?id=7 break the uniform interface. Use nouns and HTTP methods.
Storing “current page” or a cart in server memory breaks scaling and makes requests depend on each other.
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?
Show solution
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#
Which header does a client use to ask for XML instead of JSON?
Want more? Practise all ASP.NET Core Web API MCQs · ASP.NET Core Web API interview questions
My notes#
Saved in this browser.