An HTTP request is a message a client (a browser, an app or a script) sends to a server to ask it to do something: return a web page, send data, save a form or delete a record. Every request has a method such as GET or POST, a URL, headers and an optional body, and the server answers with an HTTP response containing a status code, headers and usually a body.
This guide shows what an HTTP request looks like on the wire, what each method is for, and how to make GET and POST requests with curl, JavaScript’s fetch and Python. Every example was run against a small local server (its code is at the end), and the outputs shown are real.
What Happens When You Make an HTTP Request
- The client resolves the domain name to an IP address using DNS.
- It opens a connection to the server (for HTTPS, it also performs a TLS handshake to encrypt the connection).
- It sends the request: method, path, headers and, if needed, a body.
- The server processes it and sends back a response with a status code, headers and body.
- The client uses the response, for example by rendering the HTML or reading the JSON.
HTTP is stateless: each request stands on its own, and the server does not remember previous ones. Logins and shopping carts work by sending extra information, such as a cookie or an Authorization header, with every request.
The Structure of an HTTP Request
Running curl -v http://localhost:3000/posts shows the exact request curl sent (lines starting with >) and the response it received (<):
> GET /posts HTTP/1.1
> Host: localhost:3000
> User-Agent: curl/8.7.1
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Date: Thu, 22 Oct 2026 15:03:17 GMT
< Connection: keep-alive
< Keep-Alive: timeout=5
< Transfer-Encoding: chunked
<
[{"id":1,"title":"Hello HTTP"}]A request has four parts:
| Part | Example | What it does |
|---|---|---|
| Request line | GET /posts HTTP/1.1 | The method, the path (plus any query string) and the protocol version |
| Headers | Host, Accept, Content-Type, Authorization | Metadata: which site, what formats the client accepts, what the body contains, who is asking |
| Blank line | Marks the end of the headers | |
| Body (optional) | {"title":"My first post"} | Data sent to the server, mostly with POST, PUT and PATCH |
The response mirrors it: a status line (HTTP/1.1 200 OK), headers, a blank line and the body. With HTTP/2 and HTTP/3 the same information is sent in a compressed binary form, but the methods, headers and status codes are identical, so everything in this guide applies to all three versions.
HTTP Request Methods
The method (sometimes called a verb) tells the server what you want to do. RFC 9110, the current HTTP specification, defines these, and PATCH comes from RFC 5789:
| Method | Purpose | Has a body? | Safe | Idempotent |
|---|---|---|---|---|
| GET | Retrieve a resource | No | Yes | Yes |
| POST | Send data to be processed, often creating a new resource | Yes | No | No |
| PUT | Create or replace a resource at a known URL | Yes | No | Yes |
| PATCH | Partially update a resource | Yes | No | No (not guaranteed) |
| DELETE | Delete a resource | Usually no | No | Yes |
| HEAD | Same as GET but returns only the headers | No | Yes | Yes |
| OPTIONS | Ask which methods and options a resource supports; used by browsers for CORS preflight checks | No | Yes | Yes |
| TRACE | Echo the request back for diagnostics; usually disabled for security | No | Yes | Yes |
| CONNECT | Ask a proxy to open a tunnel, for example for HTTPS through a proxy | No | No | No |
- Safe means the request should not change anything on the server, so browsers and crawlers can make it freely.
- Idempotent means sending the same request several times has the same effect as sending it once. Deleting user 42 twice still leaves user 42 deleted, but submitting the same POST twice can create two orders. That is why clients can safely retry idempotent requests after a network error, but should be careful retrying POST.
A common misconception is that POST is “secure” because the data is not in the URL. It keeps data out of browser history and server logs, but the body is only encrypted if you use HTTPS. Never send passwords or tokens over plain HTTP, whatever the method.
How to Make a GET Request
With curl
curl http://localhost:3000/posts
# [{"id":1,"title":"Hello HTTP"}]
curl -I http://localhost:3000/posts # HEAD: headers only
# HTTP/1.1 200 OK
# Content-Type: application/jsonWith JavaScript fetch
fetch is built into browsers and into Node.js 18 and later, so you don’t need a library. Use URLSearchParams to build query strings so that values are encoded correctly:
const params = new URLSearchParams({ page: '2', sort: 'new' });
const res = await fetch(`http://localhost:3000/echo?${params}`);
console.log(res.status, await res.json());
// 200 { method: 'GET', query: { page: '2', sort: 'new' }, ... }With Python
import requests
r = requests.get("http://localhost:3000/echo", params={"page": 2, "sort": "new"}, timeout=10)
print(r.status_code, r.json()["query"])
# 200 {'page': '2', 'sort': 'new'}How to Make a POST Request
To send JSON, put it in the body and set the Content-Type: application/json header so the server knows how to read it. Forgetting that header is one of the most common reasons a server says the body is empty or invalid.
With curl
curl -X POST http://localhost:3000/posts \
-H 'Content-Type: application/json' \
-d '{"title":"My first post"}'
# {"id":2,"title":"My first post"} (status 201 Created)With fetch
const created = await fetch('http://localhost:3000/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: 'Sent with fetch' }),
});
console.log(created.status, await created.json());
// 201 { id: 3, title: 'Sent with fetch' }With Python
r = requests.post("http://localhost:3000/posts", json={"title": "Sent with Python"}, timeout=10)
print(r.status_code, r.json())
# 201 {'id': 2, 'title': 'Sent with Python'}Passing json= makes requests serialize the data and set the Content-Type header for you. HTML forms send POST data differently, as application/x-www-form-urlencoded (or multipart/form-data when uploading files). If you prefer a library in JavaScript, see how to use Axios with React.
Handle Errors: fetch Does Not Throw on 404 or 500
This catches many developers out. fetch only rejects when the request cannot be made at all, such as a network failure. A 404 or 500 response is still a “successful” fetch, so you must check res.ok (true for status 200–299):
const missing = await fetch('http://localhost:3000/nope');
console.log('ok?', missing.ok, 'status', missing.status);
// ok? false status 404
if (!missing.ok) console.log('handle the error here:', await missing.json());
// handle the error here: { error: 'Not found' }In Python, call r.raise_for_status(), which raised 404 Client Error: Not Found for url: http://localhost:3000/nope in my test. Always set a timeout too; requests waits forever by default.
HTTP Status Codes You Will See Most
| Code | Meaning | Typical cause |
|---|---|---|
| 200 OK | Success | The request worked and the body has the result |
| 201 Created | A resource was created | Successful POST or PUT |
| 204 No Content | Success with no body | Successful DELETE |
| 301 / 308 | Moved permanently | The URL changed; follow the Location header |
| 304 Not Modified | Use your cached copy | Conditional GET with If-None-Match or If-Modified-Since |
| 400 Bad Request | The request is malformed | Invalid JSON, missing fields |
| 401 Unauthorized | Not authenticated | Missing or invalid credentials |
| 403 Forbidden | Authenticated but not allowed | Insufficient permissions |
| 404 Not Found | No such resource | Wrong URL or deleted item |
| 429 Too Many Requests | Rate limited | Slow down; check the Retry-After header |
| 500 Internal Server Error | The server failed | A bug or crash on the server |
| 503 Service Unavailable | Temporarily unavailable | Overload or maintenance |
Common Request Headers
| Header | Example | Purpose |
|---|---|---|
Host | example.com | Which website on the server you want (required in HTTP/1.1) |
Accept | application/json | Formats the client can handle |
Content-Type | application/json | Format of the request body |
Authorization | Bearer <token> | Credentials for an API |
Cookie | session=abc123 | Session and preference data set earlier by the server |
User-Agent | curl/8.7.1 | Which client is making the request |
Cache-Control | no-cache | Caching instructions |
Making Requests from the Browser: CORS
When JavaScript on one site calls an API on a different origin (domain, protocol or port), the browser applies the same-origin policy. The API must allow it with CORS headers such as Access-Control-Allow-Origin, and for some requests, like POST with JSON, the browser first sends an OPTIONS “preflight” request to check. If you see a CORS error in the console, the fix is on the server’s response headers; the same request from curl or Python works because CORS is enforced only by browsers.
The Test Server Used in This Guide
To reproduce the examples, save this as server.mjs and run node server.mjs (Node.js 18 or later). It has no dependencies:
import { createServer } from 'node:http';
const posts = [{ id: 1, title: 'Hello HTTP' }];
createServer(async (req, res) => {
let body = '';
for await (const chunk of req) body += chunk;
const url = new URL(req.url, 'http://localhost');
const send = (status, data) => {
res.writeHead(status, { 'Content-Type': 'application/json' });
res.end(data === undefined ? undefined : JSON.stringify(data));
};
if (url.pathname === '/posts' && ['GET', 'HEAD'].includes(req.method)) return send(200, posts);
if (url.pathname === '/posts' && req.method === 'POST') {
const post = { id: posts.length + 1, ...JSON.parse(body) };
posts.push(post);
return send(201, post);
}
if (url.pathname === '/echo') {
return send(200, { method: req.method, query: Object.fromEntries(url.searchParams), headers: { 'content-type': req.headers['content-type'] ?? null }, body });
}
send(404, { error: 'Not found' });
}).listen(3000, () => console.log('listening on 3000'));To see how to build a fuller API with validation and authentication, read how to build a REST API with Node.js and Express.
Conclusion
An HTTP request is just a method, a URL, some headers and an optional body. Use GET to read, POST to create, PUT or PATCH to update and DELETE to remove; send JSON with the right Content-Type; check the status code instead of assuming success; and always use HTTPS for anything sensitive. With curl, fetch and Python’s requests you can make any request you need, and curl -v or your browser’s Network tab will show you exactly what was sent when something goes wrong.
FAQs
What is an HTTP request?
A message a client sends to a server asking it to perform an action, such as returning a page or saving data. It contains a method (like GET or POST), a URL, headers and an optional body. The server replies with an HTTP response.
What is an example of an HTTP GET request?
Typing a URL into a browser sends a GET request. On the wire it looks like “GET /posts HTTP/1.1” followed by headers such as “Host: example.com”. From the command line, “curl https://example.com/posts” sends one.
What is the difference between GET and POST?
GET retrieves data and puts any parameters in the URL; it should not change anything on the server. POST sends data in the request body to be processed, such as creating a record. GET is safe and idempotent; POST is neither.
What is the difference between PUT and PATCH?
PUT replaces the whole resource with the data you send and is idempotent. PATCH applies a partial update, changing only the fields you include.
What are the parts of an HTTP request?
A request line (method, path and protocol version), headers, a blank line, and an optional body.
Is POST more secure than GET?
Only slightly: POST keeps data out of the URL, browser history and many server logs. Neither method encrypts anything. Use HTTPS to protect data in transit.
Why does fetch not throw an error on a 404?
fetch only rejects when the request cannot complete, such as a network error. HTTP error statuses are valid responses, so check response.ok or response.status yourself.








