# HTTP Basics

Course: [API](https://www.devtutorials.ai/learn/building-apis) · Section: REST Foundations
Source: https://www.devtutorials.ai/learn/building-apis/rest-foundations/http-basics

> Read every line of a real HTTP request and response: the method, path, query string, headers and JSON body, and why a missing Content-Type breaks a request.

**HTTP** (HyperText Transfer Protocol) is the set of rules that clients and servers use to communicate with each other over a network. Whenever an app requests data from a server or sends data to a server, it uses HTTP messages. Every API call is simply an HTTP request sent by a client and an HTTP response returned by a server. Learning HTTP helps you understand exactly how APIs work and makes it much easier to find and fix problems when something goes wrong.

## Watch an App Fail

The Brew & Bytes app sends an order. The server rejects it. The app only shows "Something went wrong", so nobody knows the real reason. The example below sends the same order three different times. Press **Run** and look carefully at each response.

- The first request succeeds with `201 Created`.
- The second request returns `415 Unsupported Media Type`. The request does not include a `Content-Type` header, so the server does not know that the body contains JSON.
- The third request returns `400 Bad Request`. The JSON data is incomplete and cannot be read correctly.

**In simple words:** the error information was already in the request and response messages. When you learn how to read those messages, you can quickly understand what went wrong instead of guessing.

## TL;DR

- **HTTP** (HyperText Transfer Protocol) is the language used by clients and servers to communicate.
- A **request** contains a start line, headers, a blank line, and an optional body.
- A **response** contains a status line, headers, a blank line, and an optional body.
- A **header** provides extra information about the request or response.
- **JSON** is the most common data format used in API bodies.
- When something fails, read the request and response before changing any code.

## A Real-World Example

Think about sending a parcel. The label tells where the parcel should go. Stickers provide extra instructions such as "Fragile" or "Handle with care". Inside the parcel is the actual item being sent. An HTTP message works in a similar way.

| Parcel | HTTP Message |
|---------|--------------|
| Address label | Start line: `POST /orders` |
| Instruction stickers | Headers such as `Content-Type: application/json` |
| Space between the label and contents | Blank line |
| The item inside | Body such as `{"item":"Latte","quantity":2}` |

If a parcel does not have the correct handling instructions, the courier may not know how to treat it. Similarly, if a request does not include `Content-Type`, the server may not know how to process the body correctly.

## What You Will Learn

In this lesson, you will learn:

1. The four parts of every HTTP request and response.
2. What methods, paths, and query strings are.
3. What headers do and why they are important.
4. How JSON is sent in the body of a request or response.
5. How to read an HTTP response manually.

In this course you read messages as plain text. Real HTTP/1.1 messages on the network look exactly like this. Newer versions (HTTP/2 and HTTP/3) pack the same parts into a faster binary format, but the parts and their meaning do not change.

## The Request

### Start Line, Headers, and Body

Every HTTP request starts with a single line called the **start line**.

The start line contains:

- The **method**
- The **path**

The **method** tells the server what action the client wants to perform.

For example:

- `GET` means "send me data".
- `POST` means "create something new".

The **path** tells the server which resource the client wants.

Examples:

```text
/menu
/orders
```

The **query string** comes after a `?` and provides extra information.

Example:

```text
/orders?table=7
```

After the start line come the **headers**.

Each header contains:

```text
Name: Value
```

Then there is a blank line. After the blank line comes the **body**, if the request includes one. The next example shows a server that simply returns information about the request it received. Press **Run**. Then try changing the path or adding a custom header such as:

```text
X-Shop: MG Road
```

The response shows:

- The request method
- The path
- The query string
- The headers

`X-App-Version` is a custom header used by the application to tell the server which app version is making the request.

### Common Headers

| Header | Purpose |
|----------|----------|
| `Accept` | Tells the server which response format the client wants |
| `Content-Type` | Tells the server what format the body uses |
| `User-Agent` | Identifies the application making the request |
| `Authorization` | Identifies the caller and permissions |

### The Query String

Small pieces of information are usually sent in the query string. For example, a client may request only menu items priced at ₹150 or less.

The first request uses:

```text
?maxPrice=150
```

Only the matching menu items are returned. The second request has no query string, so all menu items are returned. In C#, the parameter `int? maxPrice` is automatically filled from the query string. The `?` indicates that the value is optional. In Hono, `c.req.query("maxPrice")` reads the same value.

Header names do not care about capital letters. `Content-Type`, `content-type` and `CONTENT-TYPE` are the same header. Paths are different: Hono treats `/menu` and `/Menu` as two addresses, while ASP.NET Core treats them as one. Use lowercase paths and you never have to think about it.

## The Response

### Status Line, Headers, and Body

An HTTP response also follows a standard structure. It begins with a **status line**.

Example:

```text
HTTP/1.1 200 OK
```

The status line contains:

- HTTP version
- Status code
- Status message

The status code tells the client what happened. After the status line come:

1. Headers
2. A blank line
3. The body

Servers can also add custom headers. The example below adds caching information and a custom header.

Notice these headers:

- `Cache-Control`
- `X-Shop`
- `Content-Type`

`Content-Type` is added automatically because the response body contains JSON. Headers beginning with `X-` are commonly used for custom information.

### Read a Response by Hand

Before any library processes an HTTP response, it is simply text. The following example breaks a raw HTTP response into its individual parts.

The response contains:

1. A status line.
2. One or more headers.
3. A blank line.
4. A body.

When you understand these parts, debugging API problems becomes much easier.

## JSON in the Body

**JSON (JavaScript Object Notation)** is the most common format used to send data in APIs.

JSON uses:

- `{ }` for an object.
- `[ ]` for a list.
- Double quotes for text.
- Numbers without quotes.

Look again at the example near the top of this lesson.

The same JSON order was sent three times:

- With `Content-Type: application/json` and valid JSON, the order was created successfully.
- Without `Content-Type`, the server returned `415 Unsupported Media Type`.
- With incomplete JSON, the server returned `400 Bad Request`.

In ASP.NET Core, these checks happen automatically before your endpoint code runs. In Hono, you typically add these checks yourself.

**Always send `Content-Type: application/json` with a JSON body.** A missing or wrong `Content-Type` is one of the most common reasons a request fails. The error is `415 Unsupported Media Type`, and it has nothing to do with your data.

## Where You See This

- **Browser Developer Tools**: Open the **Network** tab to view requests and responses.
- **HTTP Request Files**: Visual Studio, Rider, and VS Code can send requests from `.http` files.
- **Command Line Tools**: Tools such as `curl` allow you to send requests and inspect raw HTTP responses.

## Common mistakes

| Symptom | Cause | Fix |
|---|---|---|
| `415 Unsupported Media Type` | The request has no `Content-Type`, or the wrong one | Add `Content-Type: application/json` |
| `400 Bad Request` on a JSON body | The JSON is broken: a missing quote, comma or bracket | Check the JSON. Paste it into a JSON checker |
| The server ignores your filter | There is a typo in the query string, such as `?maxprize=150` | Compare the name with the documentation of the API |
| The body arrives empty | There is no blank line between the headers and the body (when you write requests by hand) | Leave one empty line after the last header |

## Exercises

## What to Remember

- Every HTTP message has four parts: a start line, headers, a blank line, and a body.
- A request starts with a **method** and a **path**. A response starts with a **status code**.
- **Headers** carry extra information. `Content-Type` says what the body is.
- Small details go in the **query string**, after `?`.
- When something fails, read the raw messages first.

**Next:** HTTP Methods. You will learn what GET, POST, PUT, PATCH and DELETE mean, and how to make a retry safe.

var app = builder.Build();

// This endpoint sends back what it received, line by line.
app.MapGet("/echo", (HttpRequest request) =>
{
    var lines = new List<string>
    {
        "Method: " + request.Method,
        "Path: " + request.Path,
        "Query: " + request.QueryString,
        "Accept: " + request.Headers["Accept"],
        "X-App-Version: " + request.Headers["X-App-Version"]
    };
    return string.Join("\\n", lines);
});

app.Run();`;

const app = new Hono();

// This route sends back what it received, line by line.
app.get("/echo", (c) => {
  const url = new URL(c.req.url);
  const lines = [
    "Method: " + c.req.method,
    "Path: " + c.req.path,
    "Query: " + url.search,
    "Accept: " + (c.req.header("Accept") ?? ""),
    "X-App-Version: " + (c.req.header("X-App-Version") ?? ""),
  ];
  return c.text(lines.join("\\n"));
});

Accept: application/json
X-App-Version: 2.1`;

var app = builder.Build();

var orders = new List<Order>();

// The body of the request carries the order, written in JSON.
app.MapPost("/orders", (NewOrder input) =>
{
    var order = new Order(orders.Count + 101, input.Item, input.Quantity);
    orders.Add(order);
    return TypedResults.Created("/orders/" + order.Id, order);
});

app.Run();

record NewOrder(string Item, int Quantity);
record Order(int Id, string Item, int Quantity);`;

type NewOrder = { item: string; quantity: number };
type Order = { id: number; item: string; quantity: number };

const orders: Order[] = [];
const app = new Hono();

// The body of the request carries the order, written in JSON.
app.post("/orders", async (c) => {
  // Without a JSON Content-Type the server cannot know how to read the body.
  if (!(c.req.header("Content-Type") ?? "").startsWith("application/json")) {
    return c.body(null, 415);
  }
  let input: NewOrder;
  try {
    input = await c.req.json<NewOrder>();
  } catch {
    return c.body(null, 400);
  }
  const order: Order = { id: orders.length + 101, item: input.item, quantity: input.quantity };
  orders.push(order);
  c.header("Location", "/orders/" + order.id);
  return c.json(order, 201);
});

Content-Type: application/json

{"item":"Masala Chai","quantity":2}

###
POST /orders

{"item":"Masala Chai","quantity":2}

###
POST /orders
Content-Type: application/json

{"item":"Masala Chai","quantity":`;

string raw = "HTTP/1.1 200 OK\\nContent-Type: application/json; charset=utf-8\\nCache-Control: no-store\\n\\n[{\\"id\\":1,\\"name\\":\\"Latte\\",\\"price\\":220}]";

// 1. The status line is the first line.
string[] parts = raw.Split("\\n\\n", 2);
string[] head = parts[0].Split("\\n");
Console.WriteLine("Status line: " + head[0]);

// 2. Each header line is "Name: value".
foreach (string line in head.Skip(1))
{
    string[] pair = line.Split(": ", 2);
    Console.WriteLine("Header " + pair[0] + " = " + pair[1]);
}

// 3. After the blank line comes the body.
Console.WriteLine("Body: " + parts[1]);`;

const raw = "HTTP/1.1 200 OK\\nContent-Type: application/json\\nCache-Control: no-store\\n\\n[{\\"id\\":1,\\"name\\":\\"Latte\\",\\"price\\":220}]";

// 1. The status line is the first line.
const [headText, body] = raw.split("\\n\\n");
const head = headText.split("\\n");
console.log("Status line: " + head[0]);

// 2. Each header line is "Name: value".
for (const line of head.slice(1)) {
  const [name, value] = line.split(": ");
  console.log("Header " + name + " = " + value);
}

// 3. After the blank line comes the body.
console.log("Body: " + body);`;

var app = builder.Build();

var menu = new List<MenuItem>
{
    new(1, "Latte", 220),
    new(2, "Masala Chai", 120)
};

// The server can add its own headers to the response.
app.MapGet("/menu", (HttpResponse response) =>
{
    response.Headers["Cache-Control"] = "max-age=60";   // "you may keep this for 60 seconds"
    response.Headers["X-Shop"] = "Brew & Bytes, MG Road";  // a custom header
    return TypedResults.Ok(menu);
});

app.Run();

record MenuItem(int Id, string Name, int Price);`;

type MenuItem = { id: number; name: string; price: number };

const menu: MenuItem[] = [
  { id: 1, name: "Latte", price: 220 },
  { id: 2, name: "Masala Chai", price: 120 },
];

const app = new Hono();

// The server can add its own headers to the response.
app.get("/menu", (c) => {
  c.header("Cache-Control", "max-age=60"); // "you may keep this for 60 seconds"
  c.header("X-Shop", "Brew & Bytes, MG Road"); // a custom header
  return c.json(menu, 200);
});

var app = builder.Build();

var menu = new List<MenuItem>
{
    new(1, "Latte", 220),
    new(2, "Masala Chai", 120),
    new(3, "Blueberry Muffin", 150),
    new(4, "Cold Coffee", 180)
};

// ?maxPrice=150 arrives as the parameter maxPrice.
// int? means "optional": without it, every item comes back.
app.MapGet("/menu", (int? maxPrice) =>
{
    var items = maxPrice is null ? menu : menu.Where(m => m.Price <= maxPrice).ToList();
    return TypedResults.Ok(items);
});

app.Run();

record MenuItem(int Id, string Name, int Price);`;

type MenuItem = { id: number; name: string; price: number };

const menu: MenuItem[] = [
  { id: 1, name: "Latte", price: 220 },
  { id: 2, name: "Masala Chai", price: 120 },
  { id: 3, name: "Blueberry Muffin", price: 150 },
  { id: 4, name: "Cold Coffee", price: 180 },
];

const app = new Hono();

// ?maxPrice=150 arrives through c.req.query("maxPrice").
// Without it, every item comes back.
app.get("/menu", (c) => {
  const maxPrice = c.req.query("maxPrice");
  const items = maxPrice === undefined ? menu : menu.filter((m) => m.price <= Number(maxPrice));
  return c.json(items, 200);
});

###
GET /menu`;

  {
    prompt: "Read this raw response. Print two lines: \"Status: \" with just the status number, and \"Type: \" with the Content-Type value.",
    hint: "The status number is the second word of the first line. The Content-Type is on the line that starts with \"Content-Type: \".",
    explain: "The status line comes first, and then the headers. Reading them in that order is how every HTTP library works.",
    csharp: { starter: `string raw = "HTTP/1.1 404 Not Found\\nContent-Type: text/plain; charset=utf-8\\n\\nNo such order";

// Print two lines: the status code (just the number) and the Content-Type value.
// Example:
// Status: 404
// Type: text/plain; charset=utf-8
Console.WriteLine(raw);`, solution: `string raw = "HTTP/1.1 404 Not Found\\nContent-Type: text/plain; charset=utf-8\\n\\nNo such order";

// Print two lines: the status code (just the number) and the Content-Type value.
string[] lines = raw.Split("\\n");
string status = lines[0].Split(" ")[1];
string type = lines.First(l => l.StartsWith("Content-Type: ")).Substring("Content-Type: ".Length);
Console.WriteLine("Status: " + status);
Console.WriteLine("Type: " + type);`, mode: "console" },
    react: { starter: `const raw = "HTTP/1.1 404 Not Found\\nContent-Type: text/plain; charset=utf-8\\n\\nNo such order";

// Print two lines: the status code (just the number) and the Content-Type value.
// Example:
// Status: 404
// Type: text/plain; charset=utf-8
console.log(raw);`, solution: `const raw = "HTTP/1.1 404 Not Found\\nContent-Type: text/plain; charset=utf-8\\n\\nNo such order";

// Print two lines: the status code (just the number) and the Content-Type value.
const lines = raw.split("\\n");
const status = lines[0].split(" ")[1];
const type = lines.find((l) => l.startsWith("Content-Type: "))!.slice("Content-Type: ".length);
console.log("Status: " + status);
console.log("Type: " + type);`, mode: "console" }
  },
  {
    prompt: "Add a GET /hours endpoint that answers with the plain text: Open 8 am to 9 pm",
    hint: "In C#, a handler that returns a string sends text/plain. In Hono, use c.text(...).",
    explain: "Returning a plain string makes the server send Content-Type: text/plain. Before your change, the path did not exist, so the answer was 404.",
    csharp: { starter: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Add GET /hours. It should answer with plain text:
// Open 8 am to 9 pm

app.Run();`, solution: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// Add GET /hours. It should answer with plain text:
// Open 8 am to 9 pm
app.MapGet("/hours", () => "Open 8 am to 9 pm");

app.Run();`, mode: "aspnet", requests: `GET /hours` },
    react: { starter: `import { Hono } from "hono";

const app = new Hono();

// Add GET /hours. It should answer with plain text:
// Open 8 am to 9 pm

const app = new Hono();

// Add GET /hours. It should answer with plain text:
// Open 8 am to 9 pm
app.get("/hours", (c) => c.text("Open 8 am to 9 pm"));

    checks: `GET /hours?day=sunday

###
GET /hours?lang=en`
  },
  {
    prompt: "The app sends the table number in a header called X-Table. Make GET /table answer with text like: Table 7",
    hint: "In C#, read request.Headers[\"X-Table\"]. In Hono, read c.req.header(\"X-Table\").",
    explain: "Headers carry small facts about a request. The server reads them by name, and the name ignores capital letters.",
    csharp: { starter: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// The app sends the table number in a header called X-Table.
// Make GET /table answer with plain text like: Table 7
app.MapGet("/table", (HttpRequest request) => "Table ?");

app.Run();`, solution: `var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// The app sends the table number in a header called X-Table.
// Make GET /table answer with plain text like: Table 7
app.MapGet("/table", (HttpRequest request) => "Table " + request.Headers["X-Table"]);

app.Run();`, mode: "aspnet", requests: `GET /table
X-Table: 7` },
    react: { starter: `import { Hono } from "hono";

const app = new Hono();

// The app sends the table number in a header called X-Table.
// Make GET /table answer with plain text like: Table 7
app.get("/table", (c) => c.text("Table ?"));

const app = new Hono();

// The app sends the table number in a header called X-Table.
// Make GET /table answer with plain text like: Table 7
app.get("/table", (c) => c.text("Table " + (c.req.header("X-Table") ?? "")));

X-Table: 7` },
    checks: `GET /table
X-Table: 12

###
GET /table
X-Table: 3`
  }
];
