What each verb promises, why idempotency is worth caring about, and how the router decides which handler runs.
Overview
A resource is one name and several operations
The same path with different methods is the central idea of an HTTP API. /modules/7 names a thing; GET, PUT and DELETE are what you can do to it.
FastAPI gives you a decorator per verb, and registering several against one path is normal rather than a special case.
Worth knowing
One path can carry a handler per verb. That is what a resource is: one name, several operations.
A known path with an unregistered verb gives 405, not 404. The distinction tells you the URL was right.
GET is safe — it must not change anything, because browsers prefetch and proxies cache.
PUT and DELETE are idempotent: repeating them leaves the same result, so a client can safely retry after a timeout. POST is not.
Routes match in registration order, first match wins. app.routes prints the table when a route is not behaving.
api_route(methods=[...]) registers one function for several verbs. Useful for health checks; two verbs usually deserve two functions.
HTTP Methods and Routing
What each verb promises, and how the router decides which handler runs.
One path, several methods
The same URL can carry a different handler per verb. That is the shape of a resource: one name, several things you can do to it.
example_01.pyFastAPI
Output
The wrong method is a 405
A path that matches a route registered for another verb gives 405, not 404. The difference tells you the URL was right.
example_02.pyFastAPI
Output
Idempotency, demonstrated
Sending the same request twice should mean something different for POST than for PUT. Here is that difference, counted.
example_03.pyFastAPI
Output
Registration order, and how to inspect it
The router walks its table in order and takes the first match. You can print that table, which settles most routing arguments.
example_04.pyFastAPI
Output
Several methods on one function
api_route registers one handler for a list of verbs, and Request.method tells you which arrived.
example_05.pyFastAPI
Output
A resource, end to end
The five verbs on one collection, with the status codes each should return. This is the shape most CRUD endpoints converge on.
example_06.pyFastAPI
Output
The promises each verb makes
These are not conventions you may ignore. Browsers, proxies, CDNs, load balancers and HTTP client libraries all act on them, and breaking one produces behaviour you did not write.
GET is safe. It must not change anything. Browsers prefetch links, proxies cache responses, monitoring replays requests, and a "click here to delete" link behind a GET will eventually be followed by something that was not a person.
POST is not idempotent. Sending it twice does it twice. That is correct for creating, and it is why a client that times out mid-POST genuinely does not know whether it succeeded.
PUT is idempotent. It replaces a resource at a known URL, so repeating it leaves the same state. A client can retry it safely.
DELETE is idempotent. Deleting twice leaves the thing deleted. The second call returning 404 is acceptable; many APIs return 204 both times, which is friendlier to a retrying client.
PATCH is not necessarily idempotent. "Set the title to X" is; "increment the counter" is not.
That retry property is the practical payoff. It is what lets a client library automatically retry a failed PUT and refuse to retry a POST, without knowing anything about your application.
404 versus 405
A path nothing matched is a 404. A path that matched a route registered for a different verb is a 405 Method Not Allowed.
The distinction is genuinely useful when debugging: a 405 means the URL is right and the method is wrong, which is usually a typo in the decorator or a client sending POST where PUT was meant. A 404 means the path never matched at all, which is a different search.
Order, and inspecting it
Routes match in registration order and the first match wins — the rule from the first tier, and the reason a fixed path must be declared before a variable one that would also match.
app.routes is worth knowing about, because it settles arguments. Printing the table shows exactly what the router will try and in what order, which is faster than reasoning about it.
Routes registered through an APIRouter appear in the order the routers were included, which becomes relevant once an app is split up — a catch-all in an early router can shadow a specific route in a later one.
One function, several verbs
api_route takes a list of methods:
@app.api_route("/ping", methods=["GET", "HEAD"])
The honest use is a health check, where GET and HEAD should behave identically. Beyond that it is usually the wrong tool: two verbs mean two different operations with two different meanings, and one function containing if request.method == "POST" is two functions that have not been separated yet.
Designing the set
Most collections end up with five endpoints, and there is little value in being creative about them.
GET /modules lists. POST /modules creates and returns 201. GET /modules/{id} reads or 404s. PUT or PATCH /modules/{id} updates. DELETE /modules/{id} removes and returns 204.
The temptation is to add verbs to paths for anything that does not fit — /modules/{id}/publish, /modules/search. Sometimes that is right: an action that is not a create, read, update or delete genuinely needs a name, and POST /modules/{id}/publish is clearer than inventing a field whose mutation has a side effect.
What to avoid is the halfway house: POST /getModules, or GET /modules/delete/{id}. The first duplicates what the method already says; the second puts a destructive action behind a safe verb, which is the one mistake in this module with real consequences.
Trailing slashes, once more
/modules and /modules/ are different paths, and FastAPI redirects between them with a 307.
That is mostly invisible. Where it bites is a POST: a 307 preserves the method and body in theory, and some clients drop the body in practice, producing a request that arrives empty for no reason the caller can see. Pick one convention across your API and hold it.
Mistakes people make
A mutating GET. The one with real consequences. Browsers prefetch, proxies cache, monitoring replays, and link scanners follow. A "delete" behind a GET will eventually run without a person involved.
Using POST for everything. It works, and it throws away idempotency. A client that times out on a PUT can safely retry; on a POST it genuinely cannot know whether the write landed. Choosing the verb correctly gives every HTTP library in the world useful information for free.
Verbs in paths.POST /createModule duplicates what the method already says, and GET /modules/delete/7 puts a destructive action behind a safe verb.
Assuming 404 means the route is wrong. A 405 means the path matched and the method did not - usually a typo in the decorator or a client using POST where PUT was meant.
Registering a variable route before a fixed one. Still the most common routing bug, and across routers it is harder to see because the two live in different files.
Inconsistent trailing slashes./modules and /modules/ differ, and the 307 between them loses the body in some clients - producing a POST that arrives empty for no visible reason.
Idempotency in practice
The property is worth one more paragraph because it is the one people skip.
A network is unreliable in a specific way: a request can succeed while the response is lost. The client sees a timeout and has no idea whether the write happened.
For a PUT or DELETE that does not matter - retry, and the end state is the same. For a POST it matters a great deal, and the standard answer is an idempotency key: the client generates one, sends it as a header, and the server records it alongside the result so a repeat returns the original response instead of creating a second record.
Payment APIs all do this, for obvious reasons. Most other APIs should and do not, and it is much easier to add before the duplicates appear than after.
Designing a URL space
Beyond individual routes, a few decisions shape how an API feels to use.
Nouns for resources, and plural./modules, not /module or /getModules. The method supplies the verb.
Nest only for ownership./modules/{id}/lessons is right when a lesson belongs to exactly one module and has no independent identity. When it does have one, /lessons/{id} alongside is kinder - deep nesting forces callers to know a parent id they may not have.
Actions that are not CRUD get a sub-resource.POST /modules/{id}/publish is clearer than inventing a field whose mutation has side effects. Keep them few; an API that is mostly actions is an RPC interface wearing REST's clothes, and would be more honest as one.
Filters go in the query string./modules?track=maths narrows a set. /tracks/maths/modules claims a module is reachable only through one track.
Be consistent about case and separators. Lower case, hyphens where a word break is needed. Paths are case-sensitive in the standard and inconsistently handled in practice, and consistency removes a class of bug that only appears on somebody else's server.
What the router does not do
Two things worth knowing are absent.
No automatic redirect between methods. A GET to a POST-only route is a 405, not a redirect to somewhere sensible.
No wildcard fallback by default. An unmatched path is a 404 from the router with a generic body. If you want a catch-all - to serve a single-page app, say - you register one, and it must come last or it shadows everything after it.
That last point is the ordering rule at its most severe: a catch-all in an early router makes every route in every later router unreachable, which is a confusing morning.
A closing thought
The verbs are the oldest and best-specified part of an HTTP API, and the part most often treated as arbitrary.
Choosing them correctly is not pedantry. It is what lets a client library retry safely, a proxy cache correctly, a monitoring system distinguish a failure from a rejection, and a newcomer guess what an endpoint does before reading its documentation.
None of that requires anything from you except using the verb that matches what the endpoint actually does - which is information you already have.
The set worth memorising
Five endpoints per collection, and there is little value in deviating.
GET /things lists, with filters in the query string and a capped limit. POST /things creates, returns 201, and is not idempotent. GET /things/{id} reads or 404s. PUT or PATCH /things/{id} updates - the first replacing, the second partial. DELETE /things/{id} removes and returns 204.
Anything that does not fit becomes a sub-resource with a name: POST /things/{id}/publish. Keep those few. An API that is mostly named actions is an RPC interface, and would be clearer written as one than disguised as REST.
Summary
One path, a handler per verb. GET is safe and must change nothing. PUT and DELETE are idempotent, so a client can retry them after a timeout; POST is not, which is why idempotency keys exist.
A known path with an unregistered method is 405, not 404, and the difference tells you the URL was right.
Routes match in registration order and, across routers, in inclusion order. app.routes prints the table when something is not behaving.
And design the URL space once: plural nouns, shallow nesting, filters in the query string, and no verbs in paths.
Next
Headers and cookies come next - the parts of a request that describe the exchange rather than the resource - followed by the two body formats a browser form actually sends, the status codes that report what happened, the error handling that produces them, and the structure that keeps all of it navigable once there is more than one resource.
One more on safety
The safe-method rule deserves restating because it is the only item here whose violation causes real damage rather than inconvenience.
"Safe" does not mean "harmless to call once". It means the caller has not requested a change, so anything may call it, any number of times, without asking. Link previews in chat applications fetch URLs. Antivirus scanners fetch URLs. Browsers prefetch on hover. Corporate proxies fetch to inspect.
Every one of those will eventually hit a destructive GET, and the resulting incident is difficult to explain because nobody clicked anything. Putting the action behind POST or DELETE removes the entire class.
Check yourself
0 of 4
Answer without scrolling back up.
A GET request is prefetched by a browser and your handler deletes something. Whose bug is it?
Browsers prefetch, proxies cache and monitoring replays, all on the assumption that GET changes nothing. A destructive GET will eventually be called by something that was not a person.
What does a 405 tell you that a 404 does not?
405 means the URL is right and the verb is wrong - usually a typo in the decorator or a client using the wrong method. A 404 means nothing matched at all.
Why can a client safely retry a failed PUT but not a failed POST?
A repeated PUT replaces the same resource with the same content. A repeated POST creates a second record, which is why a timed-out POST leaves a client genuinely uncertain.
Where do routes registered on an APIRouter sit in match order?
Inclusion order determines match order, so a catch-all in an early router can shadow a more specific route in a later one.
Cheat sheet
HTTP Methods and Routing
The same path with different methods is the central idea of an HTTP API. /modules/7 names a thing; GET, PUT and DELETE are what you can do to it.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.