Skip to content
api2026-06-307 min read

One of the first things new backend developers get stuck on is whether an endpoint should be GET or POST. Some take the lazy route and use POST for everything. It works, sure, but you lose caching, bookmarks, CDN acceleration, and your API semantics turn to mush. The differences between GET and POST are baked into the HTTP spec, and browsers, proxies, and CDNs all rely on them. The rules aren't hard to learn, and the payoff is real. Want to see the difference in action? Fire off real requests with our API tester and compare GET and POST responses side by side.

Core Semantic Difference

The root distinction is semantics: GET means "read," POST means "submit."

| Dimension | GET | POST | |-----------|-----|------| | Primary use | Fetch resources | Create or submit data | | Idempotent | Yes (same result on repeat) | No | | Safe | Yes (should not change server state) | No | | Cacheable | Yes (browser, CDN, proxy) | No | | Bookmarkable | Yes (params in URL) | No | | Browser history | URL with params is recorded | Not recorded | | Parameter location | URL query string | Request body | | Length limit | Browser ~2k-8k chars | Effectively unlimited | | Content-Type support | application/x-www-form-urlencoded | Many (JSON, multipart, form) |

"Safe" and "idempotent" are HTTP spec terms, not the casual meaning of "secure." Safe means "doesn't modify server state." Idempotent means "repeating the request produces no additional effect."

GET in Detail

GET is designed for read-only access. Parameters ride in the URL query string:

GET /api/users?role=admin&page=2 HTTP/1.1
Host: example.com

Key GET properties:

First, GET is cacheable. Browsers, CDNs, and reverse proxies all honor Cache-Control headers to cache GET responses. Repeat visits can hit cache and skip the network round trip entirely. Second, GET is bookmarkable. The URL captures the full request, so copy and paste reproduces it exactly. Third, GET shows up in access logs. Every parameter is plaintext in the URL, recorded by nginx, Apache, or any log pipeline. Fourth, GET has length limits. Internet Explorer caps at about 2083 characters, Chrome around 20,000, but CDNs and proxies usually truncate closer to 8K. Fifth, GET must be side-effect free. A GET issued 100 times should leave the server in the same state.

POST in Detail

POST is designed for writes. Parameters go in the request body, in any format:

POST /api/users HTTP/1.1
Host: example.com
Content-Type: application/json

{"name": "Alice", "email": "user@example.com"}

Key POST properties:

First, POST is not cached. Every request reaches the server. Second, POST does not appear in the URL. Parameters live in the body, so browser history and the Referer header don't leak them. Third, POST has no hard length limit. Servers impose their own caps (like nginx's client_max_body_size), but there's no browser-level ceiling the way GET has. Fourth, POST accepts any Content-Type. JSON, multipart/form-data, application/x-www-form-urlencoded, raw binary, all fine. Fifth, POST is not idempotent. Two identical requests create two users, two orders, two charges.

A Common Misconception: Can GET Have a Body

Technically RFC 7231 permits a body on GET, but it is strongly discouraged. Here's why:

Many proxies and CDNs strip or reject GET requests with a body. Elasticsearch's early versions used GET with body for complex queries, then had to add POST as a compatibility path. Some client libraries (fetch, XMLHttpRequest in certain browser versions) silently drop the body from GET requests.

The right answer is to put complex query parameters in the URL (mind the length cap), or switch to POST. If your filter conditions genuinely don't fit in a URL, a POST to /api/users/query is a cleaner design.

curl Examples Side by Side

Here's what GET and POST look like under curl:

# GET request, params in URL
curl -X GET "https://api.example.com/users?role=admin&page=2" \
  -H "Authorization: Bearer __VG_TOKEN_5147cb533cb5__"

# POST request, params in body
curl -X POST "https://api.example.com/users" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer __VG_TOKEN_5147cb533cb5__" \
  -d '{"name":"Alice","email":"user@example.com"}'

GET has no body. POST does. Auth headers are the same in both. Typical responses:

# GET response
HTTP/1.1 200 OK
Content-Type: application/json

[{"id": 1, "name": "Admin"}, {"id": 2, "name": "Bob"}]

# POST response (created)
HTTP/1.1 201 Created
Location: /users/3
Content-Type: application/json

{"id": 3, "name": "Alice"}

Note the status code split. GET success is 200. POST success when creating is 201, with a Location header pointing to the new resource.

Method Conventions in REST

RESTful APIs map HTTP verbs to CRUD operations with fixed pairings:

| Operation | Method | Endpoint Example | Idempotent | |-----------|--------|------------------|------------| | Create | POST | POST /users | No | | Read list | GET | GET /users | Yes | | Read one | GET | GET /users/123 | Yes | | Full update | PUT | PUT /users/123 | Yes | | Partial update | PATCH | PATCH /users/123 | No (varies) | | Delete | DELETE | DELETE /users/123 | Yes |

PUT is idempotent: sending the same request twice leaves the resource in the same final state (overwritten to identical data). DELETE is idempotent too: deleting once or twice yields the same outcome, which is "the user no longer exists."

Two Classic Mistakes

Mistake One: Using POST for Reads

// Anti-pattern
app.post('/api/users/search', (req, res) => {
  const { keyword, page, filter } = req.body;
  // query DB and return results
});

Problems: no browser caching, no CDN acceleration, no bookmarks, and the browser shows a "resubmit form" dialog on refresh.

Correct version:

app.get('/api/users/search', (req, res) => {
  const { keyword, page, filter } = req.query;
  // same query logic
});

Mistake Two: Using GET for Mutations

// Anti-pattern: mutating data inside GET
app.get('/api/users/delete', (req, res) => {
  const { id } = req.query;
  db.users.delete(id);
});

Problems: violates idempotency conventions. Search engine crawlers, prefetch mechanisms, and browser preload features can all trigger the URL unexpectedly and delete the data. Google Web Light, Chrome prefetch, and SEO scanners all proactively follow links they find.

Correct version:

app.delete('/api/users/:id', (req, res) => {
  db.users.delete(req.params.id);
  res.status(204).end();
});

Security Considerations

The security difference between GET and POST is often misunderstood. POST is not more secure than GET, it just changes where parameters sit.

| Risk | GET | POST | |------|-----|------| | Browser history | Exposes params | Hidden | | Server access logs | Exposes params | Hidden | | Referer header | Leaks to third parties | Hidden | | Bookmarks | Exposes params | Hidden | | Man-in-the-middle attacks | Equally vulnerable | Equally vulnerable |

Putting passwords or tokens in GET parameters is a disaster. They land in browser history, access logs, and Referer headers, readable by anyone with log access. But this doesn't make POST "secure." On unencrypted HTTP, a MITM can read the POST body just as easily. Real security comes from HTTPS, not from the method choice.

Sensitive data always travels over HTTPS, and never in the URL. POST putting data in the body is "less exposed," but it is not encryption.

When to Use Which: Decision Table

| Scenario | Recommended | Why | |----------|-------------|-----| | List query (with filters, pagination) | GET | Cacheable, bookmarkable | | Single resource detail | GET | Cacheable | | Create new resource | POST | Not idempotent | | Full resource update | PUT | Idempotent | | Partial resource update | PATCH | More precise semantics | | Delete resource | DELETE | Idempotent | | File upload | POST (multipart) | Body can carry binary | | Complex query exceeding URL length | POST | Avoids length cap | | Side-effecting action (send email, push) | POST | Not idempotent | | Server-side search (cacheable preferred) | GET | Performance |

Testing HTTP Endpoints With DevToolkit Pro

A solid HTTP client saves a lot of time when debugging APIs. These three tools all run in your browser. Requests go directly from your browser to the target server, with no DevToolkit backend in the middle:

  • API Tester: supports GET, POST, PUT, DELETE, PATCH, custom headers, body, with timeout and cancel
  • HTTP Status Codes: look up any status code's meaning and correct usage
  • HTTP Method Cheatsheet: semantics, idempotency, and safety of every HTTP method at a glance

Because the API tester fires requests from your browser, the target server's response shows up directly in the tool. If your endpoint needs auth, the token stays in your local browser and never reaches the DevToolkit servers.

Summary

Choosing between GET and POST isn't a matter of taste, it's a matter of semantics. GET is for reads: idempotent, cacheable, bookmarkable. POST is for writes: non-idempotent, not cacheable. Writing reads as POST sacrifices caching and performance. Writing mutations as GET invites data corruption. The rule of thumb: any request that changes server state uses POST (or PUT, PATCH, DELETE), and pure reads always use GET. Pair that with HTTPS and your API has a baseline of correctness and security.


This post is brought to you by DevToolkit Pro. For more developer tools, visit the homepage.


Advertisement