Home About Me

Why “Just Use POST for Everything” Is a Bad API Design Habit

HTTP methods

There is a surprisingly persistent idea in backend development: make every API a POST, finish faster, argue less, go home earlier.

The usual justification sounds practical on the surface. POST is said to be safer over HTTPS. It avoids debating whether an endpoint should be GET, PUT, DELETE, or something else. And since work is work, some people treat consistency with HTTP semantics as unnecessary elegance.

That line of thinking is wrong.

Using POST for everything is like hiring someone to renovate your house and hearing: I only use nails. Screws, bolts, clips, latches—don’t care. One nail gun does it all. Faster, easier, safer, and I get off work early. It may sound efficient, but it is really a refusal to use the right tool for the job.

The issue here is not aesthetics. It is about whether your API is expressing business behavior clearly and whether the surrounding systems—gateways, caches, retries, monitoring, permissions, traffic controls—can work with it correctly.

HTTP methods are part of control logic

In software, there are usually two kinds of logic:

  • Business logic: the code that directly serves user needs, such as saving submitted data, fetching user information, completing an order, issuing a refund, and so on.
  • Control logic: the non-functional logic that controls how the system runs, such as loops, concurrency, distributed communication, protocols, databases, middleware, and operational behavior.

Network protocols follow the same separation. Most mainstream protocols have two layers of information:

  • a header, which carries protocol-level control information
  • a body, which carries user or business data

HTTP methods live in the protocol header, so their job is primarily about control semantics, not your business payload.

That is why REST-style APIs are expected to follow standard HTTP method semantics rather than treating methods as meaningless labels.

What the major HTTP methods mean

A practical mapping looks like this:

<table> <thead> <tr> <th>Method</th> <th>Purpose</th> <th>Idempotent</th> </tr> </thead> <tbody> <tr> <td>GET</td> <td>Querying data, similar to database select</td> <td>✔︎</td> </tr> <tr> <td>PUT</td> <td>Replacing or fully updating data, similar to update</td> <td>✔︎︎</td> </tr> <tr> <td>DELETE</td> <td>Deleting data, similar to delete</td> <td>✔︎︎</td> </tr> <tr> <td>POST</td> <td>Creating data, similar to insert</td> <td>✘</td> </tr> <tr> <td>HEAD</td> <td>Returning metadata about a resource, or probing API health</td> <td>✔︎</td> </tr> <tr> <td>PATCH</td> <td>Partially updating data</td> <td>✘</td> </tr> <tr> <td>OPTIONS</td> <td>Retrieving API capability information</td> <td>✔︎</td> </tr> </tbody> </table>

PUT and PATCH are both for updates, and both may create a resource if it does not exist. But they are not the same.

  • PUT is for replacing the entire resource representation
  • PATCH is for updating specific fields in a more API-oriented way

That distinction matters. PUT behaves like submitting the full form. PATCH behaves like sending only the changed fields. The partial update semantics are described in RFC 5789.

At the same time, you should not mechanically map everything to database CRUD.

Take /login as an example. Someone might say login checks a username and password against the database, so it is a query and should be GET. But semantically, login is not just reading data. It creates or updates authenticated session state and returns a session token. In that sense, POST makes more sense. Similarly, /logout could reasonably be modeled as DELETE.

So the point is not to force every endpoint into a crude CRUD mold. The point is to think about business semantics, not just database actions.

Idempotency is not optional in remote systems

One of the most important control-level properties of an API is idempotency.

An operation is idempotent if executing it once or multiple times produces the same result and no extra side effects.

Examples:

  • POST for creating an order is usually not idempotent
  • DELETE is idempotent because deleting something once or multiple times ends in the same state
  • PUT is idempotent because replacing a resource with the same representation repeatedly yields the same result
  • PATCH is often not idempotent, especially for operations like cnt = cnt+1

This matters a lot in distributed systems.

Remote calls can time out. When a timeout happens, the client often cannot tell whether the server never received the request, received it but did not finish, or completed it successfully and only the response was lost. In that situation, a blind retry can be dangerous.

For non-idempotent actions such as money transfers, retrying the same request may perform the operation twice. That is catastrophic.

When your API uses HTTP methods properly, the method itself gives the client and infrastructure a baseline understanding of retry safety. If every endpoint is just POST, that signal disappears. The client loses one of the most useful pieces of operational information it could have had.

HTTP methods help much more than request routing

Idempotency is only one reason to use the proper methods. Once you preserve method semantics, a lot of useful control logic becomes easier and more reliable.

Caching

API gateways and CDNs can cache GET requests naturally. Query operations are exactly where caching is most useful.

Rate limiting

Read and write traffic usually deserve different policies. A method-aware rate limit is much more precise than a one-size-fits-all rule.

Routing

Read requests and write requests may need to go to different backends—for example, read replicas versus write masters.

Authorization and auditing

Permissions can be more fine-grained when reads, writes, updates, and deletes are clearly separated.

Monitoring

Different methods often have different latency and throughput characteristics. Method-level metrics make performance analysis more meaningful.

Load testing

If everything is a POST, stress testing becomes harder to classify and reason about. You lose a basic axis for traffic modeling.

And there are more cases beyond these.

Even if your system is simple, the minimum reasonable baseline is still separating reads from writes:

  • use GET for read operations
  • use POST for write operations

That is already much better than collapsing everything into one verb.

How to handle more complex REST queries

For query-style APIs, the common needs are usually:

  • sorting
  • filtering
  • searching
  • pagination

Well-designed REST APIs already have established ways to express these in the URL.

Sorting

Use sort with a syntax like {field_name}|{asc|desc},{field_name}|{asc|desc}.

Examples:

GET /admin/companies?sort=rank|asc

GET /admin/companies?sort=rank|asc,zip_code|desc

Filtering

Simple filters can be straightforward query parameters:

GET /companies?category=banking&location=china

For more flexible expressions, you can build a filter syntax in the URL. One practical approach is to support comparison operators such as =<><=>= and logical operators such as andornot.

Because some URL characters need escaping or transformation, >= might be represented as ge, for example.

Then you can support queries like:

GET /products?$filter=name eq 'Milk' and price lt 2.55

This finds milk products priced under 2.55.

Search

Use search or a dedicated search endpoint with query terms.

Examples:

GET /books/search?description=algorithm

GET /books/search?key=algorithm

Pagination

Pagination should be the default behavior for result sets. Returning huge collections by default is rarely a good idea.

A common style is:

  • page for page number
  • per_page for page size

Example:

GET /books?page=3&per_page=20

For large datasets or frequently changing data, offset-based paging may have performance and consistency problems. In those cases, cursor-like or absolute-position paging works better, using the last seen ID or timestamp.

Examples:

GET /news?max_id=23454345&per_page=20

GET /news?published_before=2011-01-01T00:00:00Z&per_page=20

What if the query is too complex for GET?

In theory, GET can have a request body. In practice, many libraries, frameworks, and intermediaries do not support it well. That limitation is real.

A reasonable principle is this:

  1. For ordinary queries, when the parameters fit naturally in the path and query string, use GET. Filtering, sorting, and pagination alone are not a reason to switch to POST.
  2. For very complex queries, still try to use GET if you can. Only fall back to POST when your tooling or environment objectively does not support a workable GET request.

Elasticsearch has long been a useful example. Its search API accepts POST, but its authors explicitly preferred GET for search because the action is semantically about retrieval. The POST alternative existed mainly because request bodies on GET were not universally supported. Later changes in Elasticsearch itself made the situation less aligned with that original principle, but the principle remains sound: use GET for retrieval when possible.

For operations that become too complex, another option is to decompose them into multiple API calls. That increases network round trips, but it also reduces coupling between backend logic and data shape, which often fits microservice architecture better.

And if you want query expressiveness closer to GraphQL while staying in the REST family, something like OData is worth considering. OData, short for Open Data Protocol, was originally developed by Microsoft in 2007 as an open protocol for building queryable and interoperable RESTful APIs.

Common arguments for “POST everything,” and why they fail

“Why bother with REST conventions at all?”

Because RESTful HTTP APIs are not a niche preference. They are effectively a widely shared convention for how HTTP APIs work.

Once you align with that convention, you get a large amount of infrastructure value almost for free: CDNs, API gateways, service governance, monitoring, policy enforcement, and other tooling all become easier to use correctly.

Standards lower engineering cost. They help teams avoid needless mistakes. That is the real payoff.

“Isn’t this premature optimization?”

No. API design is closer to schema design than to micro-optimization.

An API is a contract. Once clients start depending on it, changing it becomes expensive. Even if you version it, you still need downstream consumers to migrate.

That is why sloppy API design is not something you can casually fix later. It tends to become permanent debt.

“Is POST more secure?”

No.

Many people believe GET is less secure because parameters appear in the URL while POST does not. That is an oversimplification.

The full HTTP request path is part of the protocol metadata. If you are using HTTPS, both the URL path and the body are protected in transit.

There are practical concerns, yes:

  • some gateways such as nginx may log URLs
  • browsers may keep URLs in history

But that still does not make POST inherently secure. In fact, some common attack patterns such as CSRF are strongly associated with unsafe state-changing requests, especially POST.

Security is a much broader topic than choosing a verb.

If your concern is sensitive data in GET, encrypt it when appropriate. The same principle applies to POST.

If your concern is tampering with query parameters, sign the URL. In fact, URL signatures are commonly used on GET, while people often forget equivalent protections on POST.

If your concern is someone tricking a user into following a malicious link, then proper authentication and authorization mechanisms such as HMAC-style request signing are what matter.

So the real answer is simple: neither GET nor POST is automatically secure. Treat security seriously regardless of method.

“Using POST everywhere saves time and reduces communication”

It does the opposite.

First, assigning proper methods usually costs almost nothing. Splitting CRUD behavior into different handlers is normal programming practice, and modern frameworks already make this easy. In many stacks, building standard CRUD endpoints is almost automatic.

Second, standards reduce onboarding and cross-team communication cost. That is what standards are for. When teams follow shared rules, new engineers understand the API faster, and external consumers do not need to ask what each endpoint really means.

Third, if your entire API is an unstructured pile of POST endpoints, anyone who uses it will have to keep asking questions. You may save a few minutes while building business logic, but you create more work everywhere else—in usage, operations, governance, retries, and long-term maintenance. That is technical debt, and it is rarely cheap.

“I just want to finish early and go home”

Cutting corners does not actually buy peace.

If your code is confusing, fragile, or easy to misuse, people will still come back to you later. They will message you after hours, ask you to explain the API, or call you in to fix production issues.

The real way to “go home early” is the long-term version:

  • organize and design your code well, so it is easier to extend
  • keep quality high, with decent documentation and comments, so others can understand and maintain it

That reduces interruptions. It lowers the cost of future changes. It makes your work sustainable.

Short-term convenience is often the enemy of long-term convenience.

“It’s just a job. Elegance doesn’t pay the bills.”

Two responses.

First, following basic conventions should not even be called elegance. It is simply the normal professional baseline. If that already counts as “too elegant,” the standard has become far too low.

Second, if you see yourself as a professional programmer, then respect for the profession matters. Part of respecting the craft is not treating it carelessly. If practitioners themselves do not care about standards, semantics, or quality, it becomes harder for others to respect the work as a true profession rather than disposable labor.

Professionalism is not only about earning a good salary. It is also about maintaining the value and credibility of the profession itself.

Your job gives you authority, but only your behavior gives you respect

Your job may give you authority. Only your behavior earns respect.