{"id":126,"date":"2026-04-20T05:02:07","date_gmt":"2026-04-20T05:02:07","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/api-design-best-practices-rest-2026\/"},"modified":"2026-04-20T05:02:07","modified_gmt":"2026-04-20T05:02:07","slug":"api-design-best-practices-rest-2026","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/api-design-best-practices-rest-2026\/","title":{"rendered":"API design best practices I keep going back to"},"content":{"rendered":"<p>I&rsquo;ve designed maybe fifteen or twenty internal APIs at this point, across different companies and stacks. I&rsquo;ve also consumed enough poorly designed ones to know exactly which mistakes feel obvious in hindsight and still get made every time.<\/p>\n<p>The turning point for me was the first time another team had to integrate with something I&rsquo;d built. Watching them hit the rough edges I hadn&rsquo;t thought about was educational in a way that code review never quite is. You learn different things when you&rsquo;re the one being integrated against.<\/p>\n<p>These are the rules I actually go back to every time I&rsquo;m designing an API from scratch. Not a comprehensive spec \u2014 a short list of things that resolve the most common arguments and prevent the most debugging time.<\/p>\n<h2 id=\"name-resources-not-actions\">Name resources, not actions<\/h2>\n<p>The most common REST API mistake is using endpoints as action names instead of resource names.<\/p>\n<pre><code># Avoid this\nPOST \/createUser\nPOST \/deleteUser?id=123\nGET  \/getUserById?id=123\nPOST \/updateUser\n\n# Do this instead\nPOST   \/users\nDELETE \/users\/{id}\nGET    \/users\/{id}\nPATCH  \/users\/{id}\n<\/code><\/pre>\n<p>The HTTP method already describes the action. Your endpoint should describe the <em>thing<\/em> being acted on. Once you internalize this, a lot of design decisions stop being decisions \u2014 you&rsquo;re just modeling your domain as nouns and letting the verbs come from HTTP.<\/p>\n<p>The tricky cases are operations that don&rsquo;t fit cleanly into CRUD. &ldquo;Archive an order&rdquo; \u2014 is that a PATCH to set <code>status: archived<\/code>? A DELETE? A POST to <code>\/orders\/{id}\/archive<\/code>? I usually go with PATCH and a status field, because it keeps the resource model clean and lets you distinguish archiving from hard deletion at the data layer. The <a href=\"https:\/\/cloud.google.com\/apis\/design\/custom_methods\" rel=\"nofollow noopener\" target=\"_blank\">Google API Design Guide<\/a> has good thinking on custom methods if you hit genuinely complex cases.<\/p>\n<h2 id=\"version-from-the-start-even-if-you-think-you-wont-need-to\">Version from the start, even if you think you won&rsquo;t need to<\/h2>\n<p>I&rsquo;ve never worked on an API that didn&rsquo;t need versioning at some point. The only question is whether you planned for it.<\/p>\n<p>Versioning in the URL (<code>\/api\/v1\/users<\/code>) is the most common approach and the most practical for most teams. Verbose but explicit \u2014 you can see exactly which version you&rsquo;re calling, and you can run v1 and v2 side by side during a transition.<\/p>\n<p>Versioning via headers (<code>Accept: application\/vnd.api+json;version=2<\/code>) is technically cleaner but harder to test in a browser or with a basic <code>curl<\/code> command. In practice, teams that use header versioning spend more time explaining how to set headers correctly than teams that use URL versioning.<\/p>\n<p>Put the version in the URL. Accept that it looks a bit clunky. Future you will be grateful.<\/p>\n<h2 id=\"return-consistent-error-shapes\">Return consistent error shapes<\/h2>\n<p>This is the thing that causes the most pain in integrations, and it&rsquo;s completely avoidable.<\/p>\n<p>Every error response from your API should have the same shape. The shape I use looks roughly like this:<\/p>\n<pre><code class=\"language-json\">{\n  &quot;error&quot;: {\n    &quot;code&quot;: &quot;VALIDATION_ERROR&quot;,\n    &quot;message&quot;: &quot;Email address is invalid&quot;,\n    &quot;field&quot;: &quot;email&quot;\n  }\n}\n<\/code><\/pre>\n<p>The <code>code<\/code> is a machine-readable string that clients can <code>switch<\/code> on. The <code>message<\/code> is human-readable text for logging or display. Optional fields like <code>field<\/code> carry context specific to the error type.<\/p>\n<p>What you want to avoid is a mix of shapes across endpoints \u2014 sometimes <code>{\"error\": \"something went wrong\"}<\/code>, sometimes <code>{\"message\": \"Invalid input\", \"errors\": [...]}<\/code>, sometimes a plain string body. This forces every client to handle every possible shape, and they usually do it poorly.<\/p>\n<p>There&rsquo;s an actual RFC for this \u2014 <a href=\"https:\/\/www.rfc-editor.org\/rfc\/rfc7807\" rel=\"nofollow noopener\" target=\"_blank\">RFC 7807 (Problem Details for HTTP APIs)<\/a> \u2014 if you want a standard to point colleagues to. Pick one shape, document it, and stick to it across every endpoint including 4xx and 5xx responses.<\/p>\n<h2 id=\"use-422-for-validation-errors-not-400\">Use 422 for validation errors, not 400<\/h2>\n<p>This comes up in code review often enough that it&rsquo;s worth a dedicated section.<\/p>\n<p>400 (Bad Request) should mean the request is malformed \u2014 invalid JSON, wrong Content-Type header, something the server can&rsquo;t parse. 422 (Unprocessable Content) is for requests that are syntactically valid but semantically wrong: a required field is missing, an email doesn&rsquo;t match the expected format, a date is in the past when it needs to be in the future.<\/p>\n<p>The distinction matters because clients can handle these differently. A 400 means &ldquo;fix how you&rsquo;re sending the request.&rdquo; A 422 means &ldquo;fix the data you&rsquo;re sending.&rdquo; Conflating them makes client-side error handling more ambiguous than it needs to be.<\/p>\n<p>Same principle applies to 404 vs 403. 404 means the thing doesn&rsquo;t exist. 403 means the thing exists but you can&rsquo;t see it. Using 404 when you mean 403 (a common privacy pattern) is a deliberate choice some APIs make \u2014 just make it deliberately, not because you mixed them up.<\/p>\n<h2 id=\"paginate-everything-that-returns-a-list\">Paginate everything that returns a list<\/h2>\n<p>Any endpoint that returns a list should be paginated, even if you currently have five items and can&rsquo;t imagine having more. Lists grow. APIs that don&rsquo;t paginate force you to add it later, which is a breaking change if clients are relying on getting everything in one shot.<\/p>\n<p>I default to cursor-based pagination for anything that might have concurrent writes:<\/p>\n<pre><code class=\"language-json\">{\n  &quot;data&quot;: [...],\n  &quot;pagination&quot;: {\n    &quot;next_cursor&quot;: &quot;eyJpZCI6MTIzfQ&quot;,\n    &quot;has_more&quot;: true\n  }\n}\n<\/code><\/pre>\n<p>Offset pagination (<code>?page=2&amp;per_page=20<\/code>) is simpler and fine for static or append-only data. For anything where items can be inserted or deleted between pages, cursor pagination avoids the skipped-row and duplicate-row bugs that offset pagination produces under concurrent load.<\/p>\n<p>The format matters less than the consistency. Use the same pagination shape across all list endpoints, or your clients will have to write different pagination logic for each one.<\/p>\n<h2 id=\"design-the-error-cases-before-the-happy-path\">Design the error cases before the happy path<\/h2>\n<p>I picked this up from a talk I half-remember and can&rsquo;t find the source for, but it&rsquo;s changed how I approach API design.<\/p>\n<p>Before writing any endpoint logic, I write down every error case I can think of: what if the resource doesn&rsquo;t exist? What if the user doesn&rsquo;t have permission? What if a required field is missing? What if two requests race on the same resource?<\/p>\n<p>Once the error cases are explicit, the happy path usually writes itself. And you end up with a much more complete error surface documented before any client hits a production edge case.<\/p>\n<p>This pairs well with thinking about idempotency keys early \u2014 if clients might retry requests (and they will), which endpoints need to be safe to call twice? Better to decide that upfront than to discover an order got charged twice after launch.<\/p>\n<p>For how error handling actually looks in the service layer behind the API, the post on <a href=\"https:\/\/abrarqasim.com\/blog\/go-error-handling-patterns-that-actually-help\" rel=\"noopener\">Go error handling patterns<\/a> covers the approach I use in the services that sit behind these endpoints.<\/p>\n<p>This week: pick one of your own APIs and check whether its error responses have a consistent shape across every endpoint. If they don&rsquo;t, that&rsquo;s probably the highest-leverage fix you can make without breaking existing clients.<\/p>\n<hr>\n<p><em>More on what I build at <a href=\"https:\/\/abrarqasim.com\" rel=\"noopener\">abrarqasim.com<\/a>.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>After designing and consuming dozens of REST APIs, these are the rules that resolve the most arguments and save the most debugging time \u2014 with before\/after examples.<\/p>\n","protected":false},"author":2,"featured_media":125,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"After designing and consuming dozens of REST APIs, these are the rules that resolve the most arguments and save the most debugging time \u2014 with before\/after examples.","rank_math_focus_keyword":"api design best practices","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[45],"tags":[100,49,101,39],"class_list":["post-126","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-programming","tag-api-design","tag-backend","tag-rest","tag-web-development"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/126","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=126"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/126\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/125"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=126"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=126"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=126"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}