GET vs POST in ASP.NET Core Web API
GET and POST are the two HTTP methods you will use most. In one sentence: GET reads data, POST sends data to the server to create something. But the differences go further: where the data travels, whether the request can be repeated safely, whether it can be cached, and what the server should reply.
This lesson uses the Products API from What is Web API. Every request and response below was captured from that running API (only the Date and Server headers are left out).
Think of a library. Asking the librarian "show me the books by this author" is a GET: you only look, nothing changes, and you can ask the same question ten times. Handing in a form to register a new member is a POST: it creates something new, and if you hand in the same form twice, you may get two memberships.
GET: read data
A GET request asks for a resource. All its input travels in the URL: in the route (/api/products/2) or in the query string (?category=accessories). A GET request has no body.
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
// GET api/products?category=Accessories&maxPrice=1000
[HttpGet]
public IActionResult GetAll(string? category, decimal? maxPrice) // read from the query string
{
var products = _store.GetAll();
if (!string.IsNullOrEmpty(category))
products = products.Where(p => p.Category.Equals(category, StringComparison.OrdinalIgnoreCase));
if (maxPrice.HasValue)
products = products.Where(p => p.Price <= maxPrice.Value);
return Ok(products);
}
// GET api/products/2
[HttpGet("{id:int}")]
public IActionResult GetById(int id) // read from the route
{
var product = _store.Find(id);
return product is null ? NotFound() : Ok(product);
}
}
[ApiController] knows that simple parameters like string, int and decimal come from the route or query string, so [FromQuery] and [FromRoute] are optional here.
Characteristics of GET
- Safe: it must not change anything on the server. Reading a product never modifies it.
- Idempotent: sending the same GET 1 time or 100 times has the same effect (none).
- Cacheable: browsers, CDNs and proxies can cache GET responses, which makes them fast.
- Bookmarkable and shareable: the whole request is in the URL, so you can copy, bookmark or share it.
- Visible: the URL, including the query string, appears in browser history, server logs and proxy logs. Never put passwords, tokens or personal data in a GET URL.
- Limited size: URLs have practical length limits (browsers and servers typically accept a few thousand characters; Kestrel's default request-line limit is 8 KB).
POST: send data to create a resource
A POST request sends data in the request body, usually as JSON, and asks the server to process it. Most often that means "create a new resource".
The data the client sends is described by a separate class, with validation rules:
using System.ComponentModel.DataAnnotations;
// What the client sends to create a product (no Id: the server generates it)
public class ProductDto
{
[Required, StringLength(100)]
public string? Name { get; set; }
[Required]
public string? Category { get; set; }
[Range(0.01, 1_000_000)]
public decimal Price { get; set; }
[Range(0, 100_000)]
public int Stock { get; set; }
}
// POST api/products
[HttpPost]
public IActionResult Create(ProductDto dto) // complex type: read from the BODY
{
var product = _store.Add(new Product
{
Name = dto.Name!, Category = dto.Category!, Price = dto.Price, Stock = dto.Stock
});
// 201 Created + a Location header pointing to the new product
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
POST /api/products
Content-Type: application/json
{ "name": "USB-C Hub", "category": "Accessories", "price": 1899, "stock": 60 }
- The status is 201 Created, not 200. This is the correct code when something new was created.
- The Location header tells the client the URL of the new resource.
CreatedAtActionbuilds it from theGetByIdroute, so the client can immediatelyGET /api/Products/4. - The body returns the created product, including the
idthe server generated.
POST is not idempotent
Let's send exactly the same POST again:
A second product (id 5) was created. Repeating a POST can repeat its effect: two products, two orders, two payments. That's why browsers warn "Confirm form resubmission" when you refresh after a POST, and why payment APIs often accept an idempotency key header, so that a retried request is not charged twice.
Validation happens automatically
With [ApiController], invalid data never reaches the action:
POST /api/products
Content-Type: application/json
{ "name": "", "price": 0 }
The Content-Type header matters
The server needs to know the format of the body. If the client forgets Content-Type: application/json (here, it sent form data instead):
A 415 error almost always means a missing or wrong Content-Type header. A GET request never has this problem, because it has no body.
GET vs POST: comparison table
| Point | GET | POST |
|---|---|---|
| Purpose | Read / retrieve data | Send data; usually create a new resource |
| Where data travels | URL: route and query string | Request body (JSON, form data, files) |
| Request body | None | Yes |
| Changes server data? | No (safe) | Yes |
| Idempotent? | Yes: repeating has no extra effect | No: repeating can create duplicates |
| Cacheable? | Yes | Not by default |
| Bookmark / share the URL? | Yes | No |
| Visible in browser history and logs? | Yes, including parameters | The URL yes, the body no |
| Data size | Limited by URL length | Large (Kestrel's default body limit is about 30 MB, configurable) |
| Data types | Text only (in the URL) | Any: JSON, XML, files, binary |
| ASP.NET Core attribute | [HttpGet] | [HttpPost] |
| Parameter source | [FromRoute], [FromQuery] | [FromBody] (or [FromForm] for forms and files) |
| Typical success code | 200 OK (404 if not found) | 201 Created (with Location header) |
Is POST more secure than GET?
Not really, and this is a common interview trap. POST hides the data from the URL, so it doesn't appear in browser history, bookmarks or server access logs. But the data is not encrypted by POST: anyone who can see the traffic can read the body just as easily as the URL. Real security comes from HTTPS, which encrypts both the URL and the body, plus authentication and authorization. Still, sensitive data such as passwords should always go in a POST body, never in a GET URL.
Calling GET and POST from clients
From C# with HttpClient
using System.Net.Http.Json;
var http = new HttpClient { BaseAddress = new Uri("http://localhost:5288/") };
// GET
List<Product>? products = await http.GetFromJsonAsync<List<Product>>("api/products?category=accessories");
// POST
var newProduct = new { Name = "USB-C Hub", Category = "Accessories", Price = 1899m, Stock = 60 };
HttpResponseMessage response = await http.PostAsJsonAsync("api/products", newProduct); // sets Content-Type for you
Console.WriteLine(response.StatusCode); // Created
Console.WriteLine(response.Headers.Location); // http://localhost:5288/api/Products/4
From JavaScript with fetch
// GET
const products = await fetch("/api/products?category=accessories").then(r => r.json());
// POST
const response = await fetch("/api/products", {
method: "POST",
headers: { "Content-Type": "application/json" }, // without this: 415
body: JSON.stringify({ name: "USB-C Hub", category: "Accessories", price: 1899, stock: 60 })
});
console.log(response.status); // 201
When to use which
- Use GET for reading: lists, details, search with a few filters, reports.
- Use POST to create resources: register a user, place an order, upload a file.
- Use POST for actions that don't fit the other methods:
POST /api/orders/15/cancel,POST /api/auth/login. - Use POST for a "search" only when the criteria are too large or complex for a URL, or contain sensitive data. Document clearly that it doesn't change anything.
- Never use GET to change data (
GET /api/products/2/deleteis wrong): crawlers, link previews and browser prefetching can trigger GET requests on their own.
Interview questions
1. What is the difference between GET and POST?
GET retrieves data and sends its parameters in the URL; it is safe, idempotent and cacheable. POST sends data in the request body to create or process something on the server; it is not idempotent, not cached by default, and can carry large or binary data.
2. What does idempotent mean? Is POST idempotent?
An operation is idempotent if repeating it gives the same result as doing it once. GET, PUT and DELETE are idempotent. POST is not: sending the same POST twice usually creates two resources.
3. Can a GET request have a body?
The HTTP specification gives a GET body no meaning, and many servers, proxies and client libraries ignore or reject it. Send GET parameters in the route or query string; if the data is too big or complex for a URL, use POST.
4. What status code should a POST that creates a resource return?
201 Created, with a Location header pointing to the new resource and usually the created resource in the body. In ASP.NET Core, use CreatedAtAction or CreatedAtRoute.
5. Why do I get 415 Unsupported Media Type on a POST?
The request body has a Content-Type the action doesn't accept, most often because the client didn't send Content-Type: application/json with a JSON body.
6. Is POST more secure than GET?
Only slightly: POST keeps data out of URLs, history and access logs. It doesn't encrypt anything. Use HTTPS for encryption, and never put secrets in GET query strings.
7. How does ASP.NET Core know where to read parameters from?
With [ApiController], complex types are bound from the body ([FromBody]), and simple types from the route or query string. You can always be explicit with [FromRoute], [FromQuery], [FromBody], [FromForm] and [FromHeader].
Summary
- GET reads data; parameters go in the URL; it is safe, idempotent, cacheable and bookmarkable.
- POST sends data in the body, usually to create a resource; it is not idempotent and not cached.
- A successful create returns 201 Created with a Location header (
CreatedAtAction). [ApiController]validates POST bodies automatically (400) and needs the rightContent-Type(otherwise 415).- POST is not encryption; use HTTPS, and keep secrets out of GET URLs.
Next: PUT vs PATCH in Web API.