Mert Özen Further With Every Line
Backend .NET Core Web API — #01

What Is an ASP.NET Core Web API? HTTP and REST Fundamentals

Mert Özen Nov 4, 2026 9 min 1 views
What Is an ASP.NET Core Web API? HTTP and REST Fundamentals

We meet the Web API through the HTTP window: request and response anatomy, methods and idempotency, status code families and resource-oriented URLs. We also lay out the series: a .NET 6 core, then version posts.

Version note: The code in this post is written for .NET 6 and runs unchanged on .NET 10.

Guess First

You send DELETE /api/products/3 to delete a product. The network hiccups, the client is not sure the request went through, and it sends the same request again. Do you get the same status code both times? And if the answers differ, can we still call DELETE "idempotent"? Keep your answer in mind; a few sections from now we will compare it with what a real application returns.

What We Mean by a Web API

A Web API is an application that other programs talk to over HTTP. There is no human on the other side; there is a mobile app, an Angular front end, another backend service or a curl command in a terminal. These clients send a request and the API answers with data, most of the time JSON. Drawing an HTML page is not the API's job; the client does that.

Think of a restaurant. The kitchen is your business rules and your database, the customer at the table is the client. The customer does not walk into the kitchen and stir the pot. They order from the waiter in the language of the menu, and the waiter takes the order to the kitchen and brings the plate back. The Web API is that waiter. The menu is the API's contract: it describes what you get when you send which data to which address with which method.

Throughout this series we will work on a single example: a small ProductCatalog API that manages products, categories and orders. Every response in this post comes from a running version of that application. We will write its code together, step by step, in the coming posts; today we only look at it from the outside, through the HTTP window.

Anatomy of an HTTP Request

HTTP is a plain text protocol. A request has three parts: a first line that says what you want to do to which resource, headers that carry extra information about the request, and, when needed, a body that carries the data. A client that wants to add a new product sends something like this:

POST /api/products HTTP/1.1
Host: localhost:5214
Content-Type: application/json
Accept: application/json

{"name":"Laptop Stand","price":640,"categoryId":2}

The first line holds the method (POST), the address (/api/products) and the protocol version. The Content-Type header says "the body I am sending you is JSON", and Accept says "I want the answer as JSON too". The empty line between headers and body is mandatory; that is how the server knows where the body starts.

The response has the same skeleton, except that the first line holds a status code instead of a method. With curl -i you can see the response headers as well:

$ curl -i http://localhost:5214/api/products
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 03 Nov 2026 21:18:27 GMT
Server: Kestrel
Transfer-Encoding: chunked

[{"id":1,"name":"Mechanical Keyboard","price":1450,"categoryId":1},{"id":2,"name":"Wireless Mouse","price":520,"categoryId":1},{"id":3,"name":"USB-C Hub","price":890,"categoryId":2}]

Notice the Server: Kestrel line. Kestrel is ASP.NET Core's built-in web server; it reads the request from the socket and hands it to your application. Also notice that the property names in the body start with a lowercase letter: name, price. On the C# side they are Name and Price; ASP.NET Core serializes with System.Text.Json and uses camelCase by default. That is the shape the JavaScript world expects, so the client needs no extra conversion.

Now let's actually send that POST request:

$ curl -i -X POST http://localhost:5214/api/products -H "Content-Type: application/json" -d '{"name":"Laptop Stand","price":640,"categoryId":2}'
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: Tue, 03 Nov 2026 21:18:27 GMT
Server: Kestrel
Location: http://localhost:5214/api/Products/4
Transfer-Encoding: chunked

{"id":4,"name":"Laptop Stand","price":640,"categoryId":2}

The answer is not 200 but 201 Created. On top of that, the Location header gives the address of the new resource. A good API does not stop at "done"; it also tells you where the thing it created lives.

HTTP Methods and Idempotency

The method is the verb that says what you want to do to the resource. In a Web API you use five of them most of the time: GET reads, POST creates a new resource, PUT replaces an existing resource with a new one from top to bottom, PATCH updates only part of it, and DELETE removes it.

Thinking about these methods in terms of two properties makes your life much easier. A safe method does not change state on the server; GET is safe, call it a thousand times and no data moves. An idempotent method leaves the server in the same state whether you call it once or ten times. The measure is not the response but the trace left on the server. Let's call each method twice in a row:

$ curl -s -o /dev/null -w "%{http_code}\n" -X DELETE http://localhost:5214/api/products/3
204
$ curl -s -o /dev/null -w "%{http_code}\n" -X DELETE http://localhost:5214/api/products/3
404
$ curl -s -o /dev/null -w "%{http_code}\n" -X PUT http://localhost:5214/api/products/2 -H "Content-Type: application/json" -d '{"name":"Wireless Mouse","price":560,"categoryId":1}'
204
$ curl -s -o /dev/null -w "%{http_code}\n" -X PUT http://localhost:5214/api/products/2 -H "Content-Type: application/json" -d '{"name":"Wireless Mouse","price":560,"categoryId":1}'
204
$ curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:5214/api/products -H "Content-Type: application/json" -d '{"name":"Webcam","price":1100,"categoryId":2}'
201
$ curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:5214/api/products -H "Content-Type: application/json" -d '{"name":"Webcam","price":1100,"categoryId":2}'
201

Here is the answer to your guess. The first DELETE returned 204 No Content, the second 404 Not Found. The responses differ, but the state on the server is the same: product 3 is gone after both requests. So DELETE is idempotent. Idempotency does not mean "you get the same response every time"; it means "repeating the request leaves no additional effect on the server".

PUT is idempotent too: we put the product into the same state twice and nothing changed. POST is not. We sent the same request twice and got two separate Webcam records. Now you know why an order POST repeated after a network hiccup opens two orders.

PATCH depends on its design. In the sample API, PATCH /api/products/1/price?amount=50 increases the price by 50. Calling it twice returned 204 both times, but the price went from 1450 to 1550. A PATCH that says "set the price to 1500" would be idempotent; one that says "add 50 to the price" is not.

Status Code Families

Instead of memorizing status codes, get into the habit of looking at the first digit. There are five families: 1xx informational, 2xx success, 3xx redirection, 4xx client error, 5xx server error. If it starts with 4 it means "you asked for something wrong"; if it starts with 5 it means "I failed to do something". Client developers act on this split: on a 4xx they show the user a message, on a 5xx they retry the request after a while.

The ones you will see most in an API are 200 OK, 201 Created, 204 No Content, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict and 500 Internal Server Error. Let's look at a lesser-known but very instructive one. What happens if we try to delete the whole product list?

$ curl -i -X DELETE http://localhost:5214/api/products
HTTP/1.1 405 Method Not Allowed
Content-Length: 0
Date: Tue, 03 Nov 2026 21:18:28 GMT
Server: Kestrel
Allow: GET, POST

The address /api/products exists, but it does not accept DELETE. That is why we got 405 instead of 404, and the Allow header told us which methods are valid at that address. ASP.NET Core did this for us; we did not write a single line of code for it. We will cover status codes, and which one to return when, in a separate post.

Resource-Oriented URL Design

The most practical idea in REST is this: the URL points to a resource and the method says what to do with it. Because the verb lives in the method, the URL does not need one. That is why the ProductCatalog API's addresses look like this:

GET    /api/products
GET    /api/products/42
POST   /api/products
PUT    /api/products/42
PATCH  /api/products/42/price
DELETE /api/products/42
GET    /api/categories/2/products

Resource names are plural nouns: products, categories. You reach a single resource by its id. When there is a natural hierarchy, like the products of a category, you express it with the path. Things that ask for "a different view of the same list", such as filtering, sorting and paging, go into the query string: /api/products?categoryId=2&sort=price.

Here are shapes you see often in the field but should avoid:

GET  /api/getProducts
POST /api/products/delete/42
GET  /api/product-list?action=create

In all three, a verb has leaked into the URL. The second is even more dangerous: because deletion happens through POST, no cache, proxy or client library can tell what the request does by looking at the method. The first and third lead every developer to invent their own vocabulary for the same resource.

Why ASP.NET Core

ASP.NET Core is Microsoft's open source, cross-platform web framework. The same code runs on Windows, Linux and macOS; running it on Linux inside a container is the most common scenario today. Thanks to the built-in Kestrel server, you do not have to put IIS in front of it.

The core pieces an API needs come out of the box: dependency injection, configuration, logging, routing, model binding, validation, authentication and authorization. They ship inside the framework as parts that know each other; you do not have to pick separate libraries and glue them together. There are also two ways to write endpoints: controllers and minimal APIs. We will see both in this series, with the emphasis on controllers, which still dominate enterprise projects.

How This Series Is Structured

The series has two blocks. The first block is the core: we teach the fundamental concepts of ASP.NET Core Web API with the .NET 6 API surface. Every example in this block compiles on .NET 6 and runs unchanged on .NET 10. We verify this by actually building and running it on both.

Why .NET 6? Released on November 8, 2021, .NET 6 brought the minimal hosting model with a single Program.cs file. From then until .NET 10, the application's skeleton stayed the same; later versions added new pieces to that skeleton. Support for .NET 6 ended on November 12, 2024, but there are plenty of projects in the field written with .NET 6, and being able to read and migrate them is a skill of its own. Once you have the version-independent foundation in place, it is much easier to slot in what later versions bring.

The second block is evolution through versions. First we will look at the Startup.cs world of .NET Core 3.1 and .NET 5 so you can read older projects. Then there will be one post each for .NET 7, 8, 9 and 10. In each one we compare "this is how we did it in .NET 6, this is how it is done now" and add a short migration note. In the core posts, too, when a later feature is relevant we will point to it in a single sentence.

A look at the calendar: support for .NET 8 and .NET 9 ends together on November 10, 2026. .NET 10 is an LTS release supported until November 14, 2028. .NET 11 is planned for November 2026; when it ships it will be added to the series as a separate post.

Common Pitfalls

Returning 200 OK for everything. APIs that write "success": false into the body on error and leave the status code at 200 are still very common. The client then has to open every response and look inside, and monitoring tools cannot see the error. Let the status code tell the real story.

Changing data with GET. An address like GET /api/products/42/delete silently deletes data when a browser prefetch or a search engine bot triggers it. GET must stay safe.

Assuming POST is safe to repeat. For operations like payments or orders, a client resending the same POST means a duplicate record. You need to think about this at design time.

Mixing up 404 and 405. If the address does not exist you get 404; if the address exists but the method is wrong you get 405. This distinction saves you half an hour when debugging.

A Small Experiment

★☆☆ From your terminal, send a request with curl -i to any public API (for example GitHub's https://api.github.com/users/octocat). Note the status code, the Content-Type header and the casing of the property names in the body.

★★☆ Design resource-oriented URLs for a library application: books, authors, an author's books, borrowing a book and returning it. Think about how to express borrowing without a verb.

★★★ For each of the following operations, write which method you would choose and whether it is idempotent: adding a product to the cart, setting a cart item's quantity to 3, increasing the quantity by one, emptying the cart.

To run the application that produced these responses on your own machine, you first need an environment. In the next post we install the .NET SDK, manage multiple SDKs with global.json, create our first project with dotnet new webapi and send our first request.