HTTP Methods
HTTP methods are the verbs of your API. Choosing the right one tells clients, caches and proxies exactly what a request will do.
On this page · 6 min · Beginner
On this page
- 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
- 6 min
- Difficulty
- Beginner
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Use GET, POST, PUT, PATCH and DELETE correctly
- Explain safe and idempotent methods
- Map methods to actions with [HttpGet], [HttpPost] and friends
- Implement a partial update with PATCH
Explanation#
GET reads data and must never change it. POST creates a new resource (or triggers an action). PUT replaces a whole resource. PATCH changes only some fields. DELETE removes a resource.
Safe methods do not change anything (GET, HEAD). Idempotent methods give the same result if repeated: GET, PUT and DELETE are idempotent; POST is not — sending it twice creates two orders.
Web API 2 picks the HTTP method from attributes ([HttpGet], [HttpPost], [HttpPut], [HttpPatch], [HttpDelete]) or, without attributes, from the method name prefix (GetProduct, PostProduct).
PUT expects the complete object; missing fields become empty. PATCH expects only the fields to change. Web API 2 has no built-in JSON Patch, so a simple PATCH takes a DTO whose properties are nullable.
Successful DELETE and PUT commonly return 204 No Content; POST returns 201 Created.
Syntax#
Method Purpose Safe Idempotent Typical success
GET read yes yes 200 OK
POST create no no 201 Created
PUT replace whole no yes 204 No Content / 200
PATCH update part no no* 200 OK
DELETE remove no yes 204 No Content
Code example: All five verbs on one resource#
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web.Http;
using ShopApi.Models;
public class ProductPatch
{
public string Name { get; set; } // null = "do not change"
public decimal? Price { get; set; }
}
[RoutePrefix("api/products")]
public class ProductsController : ApiController
{
private static readonly Dictionary<int, Product> Store = new Dictionary<int, Product>
{
[1] = new Product { Id = 1, Name = "Pen", Price = 20m },
};
[HttpGet, Route("{id:int}", Name = "GetProduct")]
public IHttpActionResult Get(int id)
{
Product product;
if (!Store.TryGetValue(id, out product)) return NotFound();
return Ok(product);
}
[HttpPost, Route("")]
public IHttpActionResult Post(Product product)
{
product.Id = Store.Keys.Max() + 1;
Store[product.Id] = product;
return CreatedAtRoute("GetProduct", new { id = product.Id }, product);
}
[HttpPut, Route("{id:int}")]
public IHttpActionResult Put(int id, Product product)
{
if (!Store.ContainsKey(id)) return NotFound();
product.Id = id;
Store[id] = product; // replaces everything
return StatusCode(HttpStatusCode.NoContent);
}
[HttpPatch, Route("{id:int}")]
public IHttpActionResult Patch(int id, ProductPatch changes)
{
Product product;
if (!Store.TryGetValue(id, out product)) return NotFound();
if (changes.Name != null) product.Name = changes.Name;
if (changes.Price.HasValue) product.Price = changes.Price.Value;
return Ok(product);
}
[HttpDelete, Route("{id:int}")]
public IHttpActionResult Delete(int id)
{
if (!Store.Remove(id)) return NotFound();
return StatusCode(HttpStatusCode.NoContent);
}
}
POST /api/products {"Name":"Bag","Price":650} → 201 Created, Location: /api/products/2
PATCH /api/products/2 {"Price":599} → 200 {"Id":2,"Name":"Bag","Price":599.0,...}
PUT /api/products/2 {"Name":"Big Bag"} → 204 (Price is now 0 — PUT replaces the whole product)
DELETE /api/products/2 → 204
DELETE /api/products/2 → 404 (already gone)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#
- Each action declares its verb and route with attributes.
- POST assigns a new id and answers 201 with a link built from the route named "GetProduct".
- PUT replaces the stored product, so any field the client leaves out is lost.
- PATCH only touches the fields that were sent (non-null).
- DELETE returns 204 the first time and 404 afterwards — the resource state is the same either way, which is what idempotent means.
Real-world use case#
A mobile app retries requests when the network drops. Because PUT and DELETE are idempotent, retries are safe; for POST /api/orders the API accepts an Idempotency-Key header so a retried order is not created twice.
Common mistakes#
GET /api/orders/5/cancel can be triggered by crawlers, link previews or prefetching. Use POST or DELETE for changes.
Sending only {"Price": 599} with PUT wipes the other fields. Use PATCH for partial changes.
Web API throws “Multiple actions were found that match the request”. Give each a distinct route or verb.
Best practices#
- GET never changes state
- POST → 201 with Location; DELETE → 204
- Use nullable DTO properties for PATCH
- Make POST safe to retry with idempotency keys where money is involved
Remember this#
GET read · POST create · PUT replace · PATCH change part · DELETE remove.
Idempotent = same result if repeated (GET, PUT, DELETE).
PUT without a field = that field is wiped.
Practice exercise#
Your task: Write a TodosController with GET api/todos, POST api/todos (201) and DELETE api/todos/{id} (204 or 404), storing todos in a static Dictionary<int, string>.
Need a hint?
Show solution
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web.Http;
[RoutePrefix("api/todos")]
public class TodosController : ApiController
{
private static readonly Dictionary<int, string> Todos = new Dictionary<int, string> { [1] = "Learn REST" };
[HttpGet, Route("")]
public IHttpActionResult GetAll() => Ok(Todos);
[HttpGet, Route("{id:int}", Name = "GetTodo")]
public IHttpActionResult Get(int id)
{
string text;
return Todos.TryGetValue(id, out text) ? (IHttpActionResult)Ok(text) : NotFound();
}
[HttpPost, Route("")]
public IHttpActionResult Post([FromBody] string text)
{
int id = Todos.Keys.DefaultIfEmpty(0).Max() + 1;
Todos[id] = text;
return CreatedAtRoute("GetTodo", new { id }, text);
}
[HttpDelete, Route("{id:int}")]
public IHttpActionResult Delete(int id) =>
Todos.Remove(id) ? (IHttpActionResult)StatusCode(HttpStatusCode.NoContent) : NotFound();
}
Mini quiz#
Which method should create a new order?
Want more? Practise all ASP.NET Web API MCQs · ASP.NET Web API interview questions
My notes#
Saved in this browser.