<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bry</title>
    <description>The latest articles on DEV Community by Bry (@brywritescode).</description>
    <link>https://dev.to/brywritescode</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4009547%2Fe6e5c498-b824-493d-b06e-fef6445e4ef7.jpeg</url>
      <title>DEV Community: Bry</title>
      <link>https://dev.to/brywritescode</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/brywritescode"/>
    <language>en</language>
    <item>
      <title>API Gateway Patterns: AWS vs Azure vs GCP</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Tue, 18 Aug 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/api-gateway-patterns-aws-vs-azure-vs-gcp-4g1l</link>
      <guid>https://dev.to/brywritescode/api-gateway-patterns-aws-vs-azure-vs-gcp-4g1l</guid>
      <description>&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AWS API Gateway&lt;/strong&gt; comes in two flavors: HTTP API (cheaper, faster, 90% of use cases) and REST API (full feature set, higher cost). Use HTTP API unless you need REST API's request/response transformation or private integrations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure API Management (APIM)&lt;/strong&gt; is the only managed gateway with a built-in developer portal and a policy engine powerful enough to handle enterprise API governance without custom code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GCP offers three overlapping products&lt;/strong&gt;: API Gateway (serverless, OpenAPI-driven), Cloud Endpoints (gRPC and self-managed), and Apigee (enterprise-grade). Don't pick randomly — they are not interchangeable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Common patterns&lt;/strong&gt; — BFF (Backend for Frontend), API aggregation, and protocol translation — are cloud-agnostic in design but differ significantly in implementation cost across providers.&lt;/li&gt;
&lt;li&gt;Cold starts on Lambda-backed gateways, APIM's XML policy syntax, and GCP's per-project quota model are the three gotchas that will cost you time in production.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Picking an API gateway is a three-to-five year decision. Migrating between providers requires rewriting auth flows, rate-limiting config, and deployment pipelines — not just updating a URL. I've evaluated all three in production environments — migrating a client from AWS REST API to HTTP API, standing up APIM for an enterprise API program, and routing Cloud Run services through GCP API Gateway — and the differences that matter are rarely the ones in the marketing comparison tables. Most teams pick the gateway that matches their existing cloud provider without comparing what they're getting.&lt;/p&gt;

&lt;p&gt;That's often the right call. But it helps to know what you're trading. AWS, Azure, and GCP have different design philosophies: AWS optimizes for serverless-native integration, Azure optimizes for enterprise API governance, and GCP gives you a choice between a lightweight managed service and a full API platform.&lt;/p&gt;

&lt;p&gt;This article explains what each gateway does architecturally, compares them on the features that matter in production, covers the three patterns you'll implement on any of them, and flags the gotchas that each provider's documentation glosses over.&lt;/p&gt;




&lt;h2&gt;
  
  
  What an API Gateway Does
&lt;/h2&gt;

&lt;p&gt;Before comparing providers, align on the job a gateway performs. An API gateway sits between clients and backend services and handles cross-cutting concerns so your services don't have to.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffay1pjl2t07w6hfyfygi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffay1pjl2t07w6hfyfygi.png" alt="What an API Gateway Does" width="800" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: Client traffic enters the gateway, which handles auth and routing before forwarding to backend services.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The five jobs a gateway performs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Routing&lt;/strong&gt; — match request paths to backend services, including path rewriting and load balancing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication and authorization&lt;/strong&gt; — validate JWT tokens, API keys, or OAuth flows before traffic reaches your service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; — enforce per-client or per-route request quotas; protect backends from traffic spikes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request/response transformation&lt;/strong&gt; — rewrite headers, translate between protocols (REST ↔ gRPC), strip or inject fields.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt; — emit access logs, request metrics, and distributed traces without instrumenting each service.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every provider covers these five jobs. How they expose them — and at what cost — is what differentiates them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Request Flow Through a Gateway
&lt;/h2&gt;

&lt;p&gt;Before choosing a provider, understand the sequence every request traverses. The auth, rate-limit, and routing steps happen in this order regardless of which gateway you use.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo4ddfzxae8wqsn4c20uk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo4ddfzxae8wqsn4c20uk.png" alt="Request Flow Through a Gateway" width="799" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: Request flow through a gateway — auth and rate limiting happen before the backend ever receives the request.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  AWS API Gateway
&lt;/h2&gt;

&lt;p&gt;AWS offers three API products under the API Gateway brand: &lt;strong&gt;HTTP API&lt;/strong&gt;, &lt;strong&gt;REST API&lt;/strong&gt;, and &lt;strong&gt;WebSocket API&lt;/strong&gt;. HTTP API and REST API are the ones backend engineers choose between daily.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTP API vs REST API
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;HTTP API&lt;/th&gt;
&lt;th&gt;REST API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Price&lt;/td&gt;
&lt;td&gt;$1.00 / million requests&lt;/td&gt;
&lt;td&gt;$3.50 / million requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency overhead&lt;/td&gt;
&lt;td&gt;~6 ms&lt;/td&gt;
&lt;td&gt;~11 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JWT authorizer (native)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No — Lambda authorizer required&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request/response transforms&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (mapping templates)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Usage plans / API keys&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private integrations (VPC Link)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebSocket&lt;/td&gt;
&lt;td&gt;No — separate product&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS WAF integration&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use HTTP API&lt;/strong&gt; for Lambda-backed services, JWT-authenticated endpoints, and any new API where you don't need request/response mapping templates. It costs 71% less than REST API and imposes half the latency overhead. I default to HTTP API for every new AWS project — the only time I've reached for REST API in the last two years was when a client needed usage plans for billing third-party API consumers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use REST API&lt;/strong&gt; when you need usage plans and API keys for third-party developer access, request/response transformation via mapping templates, or Cognito user pool authorizers without a Lambda function.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lambda Integration
&lt;/h3&gt;

&lt;p&gt;The typical AWS pattern connects API Gateway directly to Lambda. The gateway acts as the event source; Lambda handles the business logic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// AWS Lambda handler — receives API Gateway proxy event&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyEventV2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyResultV2&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;aws-lambda&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;APIGatewayProxyEventV2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;APIGatewayProxyResultV2&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathParameters&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/problem+json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.example.com/errors/400&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bad Request&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;userId path parameter is required&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Business logic here&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# HTTP API route configuration (AWS SAM / CloudFormation)&lt;/span&gt;
&lt;span class="na"&gt;Resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;GetUserFunction&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless::Function&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Handler&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dist/handler.handler&lt;/span&gt;
      &lt;span class="na"&gt;Runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nodejs22.x&lt;/span&gt;
      &lt;span class="na"&gt;Events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;GetUser&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HttpApi&lt;/span&gt;
          &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;Path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/users/{userId}&lt;/span&gt;
            &lt;span class="na"&gt;Method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GET&lt;/span&gt;
            &lt;span class="na"&gt;Auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;Authorizer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;JWTAuthorizer&lt;/span&gt;

  &lt;span class="na"&gt;MyHttpApi&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;AWS::Serverless::HttpApi&lt;/span&gt;
    &lt;span class="na"&gt;Properties&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;Auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Authorizers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;JWTAuthorizer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;JwtConfiguration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;Audience&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.example.com"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
              &lt;span class="na"&gt;Issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://auth.example.com/"&lt;/span&gt;
            &lt;span class="na"&gt;IdentitySource&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$request.header.Authorization"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Cold Start Gotcha
&lt;/h3&gt;

&lt;p&gt;Lambda-backed HTTP APIs incur cold start latency when a function instance is not warm. For a Node.js 22.x Lambda with a small bundle, cold starts add 200–800 ms. For a Java or .NET Lambda with a heavy runtime, expect 1–5 seconds.&lt;/p&gt;

&lt;p&gt;Mitigations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enable &lt;strong&gt;Provisioned Concurrency&lt;/strong&gt; on the Lambda — eliminates cold starts for pre-warmed instances, billed hourly even when idle.&lt;/li&gt;
&lt;li&gt;Keep Lambda bundle size under 5 MB (smaller = faster cold start).&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;SnapStart&lt;/strong&gt; for Java Lambda functions (zero cold start after initial activation).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cold starts do not affect AWS ECS or EKS backends connected via VPC Link — they only affect Lambda integrations. I've seen this burn teams who built their entire p99 latency budget around gateway benchmarks, then went live and got paged at 3 AM because a burst of new connections spiked p99 to 2.8 seconds — the Lambda was a .NET 6 function with a 40 MB bundle and no Provisioned Concurrency.&lt;/p&gt;




&lt;h2&gt;
  
  
  Azure API Management (APIM)
&lt;/h2&gt;

&lt;p&gt;Azure APIM is architecturally different from AWS API Gateway. It is not a simple proxy — it is a full API management platform with three components:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuln3kes78xb5n5zodc8y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fuln3kes78xb5n5zodc8y.png" alt="Azure API Management (APIM)" width="799" height="531"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: APIM's three-component architecture — the gateway, the management plane, and the developer portal are separate concerns.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Policy Engine
&lt;/h3&gt;

&lt;p&gt;APIM's differentiating feature is its XML-based policy engine. Policies apply at four scopes: global, product, API, and operation. They execute in order: inbound → backend → outbound → on-error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- APIM policy: validate JWT, rate-limit, inject backend header --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;policies&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;inbound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Validate JWT from Authorization header --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;validate-jwt&lt;/span&gt; &lt;span class="na"&gt;header-name=&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt; &lt;span class="na"&gt;failed-validation-httpcode=&lt;/span&gt;&lt;span class="s"&gt;"401"&lt;/span&gt;
                  &lt;span class="na"&gt;failed-validation-error-message=&lt;/span&gt;&lt;span class="s"&gt;"Unauthorized"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;openid-config&lt;/span&gt; &lt;span class="na"&gt;url=&lt;/span&gt;&lt;span class="s"&gt;"https://auth.example.com/.well-known/openid-configuration"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;audiences&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;audience&amp;gt;&lt;/span&gt;https://api.example.com&lt;span class="nt"&gt;&amp;lt;/audience&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/audiences&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/validate-jwt&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Rate limit: 100 calls per 60 seconds per subscription key --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;rate-limit-by-key&lt;/span&gt; &lt;span class="na"&gt;calls=&lt;/span&gt;&lt;span class="s"&gt;"100"&lt;/span&gt; &lt;span class="na"&gt;renewal-period=&lt;/span&gt;&lt;span class="s"&gt;"60"&lt;/span&gt;
                       &lt;span class="na"&gt;counter-key=&lt;/span&gt;&lt;span class="s"&gt;"@(context.Subscription.Id)"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- Inject caller identity into backend header --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"X-User-Id"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;@(context.Request.Headers.GetValueOrDefault("Authorization","")
               .Replace("Bearer ",""))&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/inbound&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;backend&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/backend&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;outbound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Strip internal headers before returning to client --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"X-Internal-Trace-Id"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"delete"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/outbound&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;on-error&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/on-error&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/policies&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The policy engine handles authentication, rate limiting, caching, transformation, and error handling — without custom code. For an enterprise that manages 50+ APIs across multiple teams, that centralization is the primary value proposition.&lt;/p&gt;

&lt;h3&gt;
  
  
  Developer Portal
&lt;/h3&gt;

&lt;p&gt;APIM includes a fully customizable developer portal out of the box. Developers browse APIs, subscribe to products, generate API keys, and test endpoints — all without involving the API team. AWS and GCP's native gateways require third-party tooling (e.g., Backstage, Stoplight) to achieve the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pricing Tiers
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Price (approx.)&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Consumption&lt;/td&gt;
&lt;td&gt;$3.50 / million calls&lt;/td&gt;
&lt;td&gt;Pay-per-use. No SLA for developer portal.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Developer&lt;/td&gt;
&lt;td&gt;~$50/month&lt;/td&gt;
&lt;td&gt;Dev/test only. No production SLA.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;~$150/month&lt;/td&gt;
&lt;td&gt;Up to 1M calls/month included.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;~$750/month&lt;/td&gt;
&lt;td&gt;Full features, 99.9% SLA.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premium&lt;/td&gt;
&lt;td&gt;~$3,800+/month&lt;/td&gt;
&lt;td&gt;Multi-region, VNET integration, 99.95% SLA.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Consumption tier looks attractive but lacks the developer portal SLA and several advanced policy features. Standard is the minimum for production workloads with enterprise requirements. In practice, I recommend clients skip Consumption entirely for anything customer-facing — the cost delta between Consumption and Standard is irrelevant next to the engineering hours you'll spend working around its limitations.&lt;/p&gt;




&lt;h2&gt;
  
  
  GCP: API Gateway, Cloud Endpoints, and Apigee
&lt;/h2&gt;

&lt;p&gt;GCP's three API products have overlapping names and confusingly similar descriptions. Choose the wrong one and you'll hit feature walls or pay for enterprise features you don't need.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Product&lt;/th&gt;
&lt;th&gt;Best for&lt;/th&gt;
&lt;th&gt;OpenAPI&lt;/th&gt;
&lt;th&gt;gRPC&lt;/th&gt;
&lt;th&gt;Developer portal&lt;/th&gt;
&lt;th&gt;Pricing model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API Gateway&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Serverless backends (Cloud Run, Functions)&lt;/td&gt;
&lt;td&gt;Yes (OAS v2)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Per-call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cloud Endpoints&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;gRPC services, self-managed gateway&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Per-call&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Apigee&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Enterprise, partner APIs, monetization&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Subscription&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use API Gateway&lt;/strong&gt; when your backend runs on Cloud Run, Cloud Functions, or App Engine and you want a fully managed gateway with no infrastructure to operate. Import your OpenAPI spec and Google provisions the gateway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Cloud Endpoints&lt;/strong&gt; when your services speak gRPC, or when you need to host the gateway proxy (ESPv2) on your own runtime for private networking. Cloud Endpoints is also the right choice for local development with gRPC tooling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use Apigee&lt;/strong&gt; when you need a developer portal, API monetization, advanced bot detection, or multi-cloud/hybrid deployment. Apigee is the only GCP option that competes directly with Azure APIM.&lt;/p&gt;

&lt;h3&gt;
  
  
  GCP API Gateway — OpenAPI-Driven Configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# openapi.yaml — GCP API Gateway configuration&lt;/span&gt;
&lt;span class="na"&gt;swagger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.0"&lt;/span&gt;
&lt;span class="na"&gt;info&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;User Service API&lt;/span&gt;
  &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0"&lt;/span&gt;
&lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api.example.com&lt;/span&gt;
&lt;span class="na"&gt;schemes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;
&lt;span class="na"&gt;produces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;

&lt;span class="na"&gt;x-google-backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://user-service-xyz-uc.a.run.app&lt;/span&gt;
  &lt;span class="na"&gt;deadline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30.0&lt;/span&gt;

&lt;span class="na"&gt;securityDefinitions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;firebase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;authorizationUrl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
    &lt;span class="na"&gt;flow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;implicit&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oauth2&lt;/span&gt;
    &lt;span class="na"&gt;x-google-issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://securetoken.google.com/my-project"&lt;/span&gt;
    &lt;span class="na"&gt;x-google-jwks_uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com"&lt;/span&gt;
    &lt;span class="na"&gt;x-google-audiences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my-project"&lt;/span&gt;

&lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="s"&gt;/users/{userId}&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;get&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Get user by ID&lt;/span&gt;
      &lt;span class="na"&gt;operationId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;getUser&lt;/span&gt;
      &lt;span class="na"&gt;parameters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;userId&lt;/span&gt;
          &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;path&lt;/span&gt;
          &lt;span class="na"&gt;required&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;firebase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[]&lt;/span&gt;
      &lt;span class="na"&gt;responses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;200"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;User found&lt;/span&gt;
        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;401"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Unauthorized&lt;/span&gt;
        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;404"&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Not found&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Deploy to GCP API Gateway&lt;/span&gt;
gcloud api-gateway api-configs create user-service-v1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--api&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;user-service &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--openapi-spec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;openapi.yaml &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-project &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--backend-auth-service-account&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;api-gateway-sa@my-project.iam.gserviceaccount.com

gcloud api-gateway gateways create user-service-gateway &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--api&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;user-service &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--api-config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;user-service-v1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-central1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The GCP Quota Gotcha
&lt;/h3&gt;

&lt;p&gt;GCP API Gateway enforces quotas at the project level. If you run multiple APIs or environments in the same GCP project, they share quota limits. A traffic spike on one API eats into the quota headroom for every other API in that project. I've seen this catch teams when a batch job saturated API quotas during an off-hours data migration, which cascaded into 429s on the customer-facing API living in the same project — a production incident that had nothing to do with the customer-facing service itself.&lt;/p&gt;

&lt;p&gt;Structure your GCP projects to isolate production APIs. One project per environment (dev, staging, prod) is the minimum. One project per service in production is safer for high-traffic APIs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Feature Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;AWS HTTP API&lt;/th&gt;
&lt;th&gt;AWS REST API&lt;/th&gt;
&lt;th&gt;Azure APIM (Standard)&lt;/th&gt;
&lt;th&gt;GCP API Gateway&lt;/th&gt;
&lt;th&gt;GCP Apigee&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth: JWT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Native&lt;/td&gt;
&lt;td&gt;Lambda authorizer&lt;/td&gt;
&lt;td&gt;Policy (validate-jwt)&lt;/td&gt;
&lt;td&gt;OpenAPI extension&lt;/td&gt;
&lt;td&gt;Policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth: API keys&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Auth: OAuth 2.0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Partial (JWT)&lt;/td&gt;
&lt;td&gt;Lambda authorizer&lt;/td&gt;
&lt;td&gt;Policy&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rate limiting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes (usage plans)&lt;/td&gt;
&lt;td&gt;Yes (per subscription)&lt;/td&gt;
&lt;td&gt;Yes (per consumer)&lt;/td&gt;
&lt;td&gt;Yes (advanced)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Request transform&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (mapping templates)&lt;/td&gt;
&lt;td&gt;Yes (policy engine)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Response transform&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Protocol translation&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (REST ↔ SOAP)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Developer portal&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes (built-in)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;gRPC support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Via Endpoints&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WebSocket&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Separate product&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Multi-region active-active&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No (regional)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Premium tier&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pricing model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Per-call&lt;/td&gt;
&lt;td&gt;Per-call&lt;/td&gt;
&lt;td&gt;Tier (monthly)&lt;/td&gt;
&lt;td&gt;Per-call&lt;/td&gt;
&lt;td&gt;Subscription&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cold starts&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Lambda-dependent&lt;/td&gt;
&lt;td&gt;Lambda-dependent&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Common Patterns
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Backend for Frontend (BFF)
&lt;/h3&gt;

&lt;p&gt;The BFF pattern uses a dedicated gateway layer per client type — one for the web app, one for the mobile app — to avoid forcing different clients to negotiate a single general-purpose API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm9iuc8lwxhqcg66ccjba.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm9iuc8lwxhqcg66ccjba.png" alt="Backend for Frontend (BFF)" width="800" height="354"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: BFF pattern — each client type gets a dedicated aggregation layer tailored to its data needs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A BFF does data aggregation and transformation — it is not where business logic lives. Business rules belong in the downstream services.&lt;/p&gt;

&lt;p&gt;On AWS, implement each BFF as a separate Lambda function or ECS service behind its own HTTP API route. On Azure, use APIM products to route mobile vs web clients to different backends. On GCP, deploy separate Cloud Run services and separate API Gateway configurations.&lt;/p&gt;

&lt;h3&gt;
  
  
  API Aggregation
&lt;/h3&gt;

&lt;p&gt;Aggregation collapses multiple downstream service calls into a single response for the client. The gateway (or a BFF) fans out the requests in parallel and merges the results.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// API aggregation pattern — call multiple services in parallel&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;OrderSummary&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;shipping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * GET /orders/:id/summary — returns a merged view of order, inventory, and shipping data.
 *
 * @remarks
 * Fans out to three downstream services in parallel. If any service fails,
 * the endpoint returns 502 with the name of the failing service.
 *
 * @returns Merged order summary or 502 with failing service name.
 */&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders/:id/summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;orderRes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inventoryRes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shippingRes&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`http://order-service/orders/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`http://inventory-service/stock/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`http://shipping-service/shipments/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;order&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orderRes&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inventory&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;inventoryRes&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shipping&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shippingRes&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`upstream_failure`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrderSummary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderRes&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;PromiseFulfilledResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inventoryRes&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;PromiseFulfilledResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;shipping&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;shippingRes&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;PromiseFulfilledResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Protocol Translation
&lt;/h3&gt;

&lt;p&gt;Protocol translation converts between API protocols at the gateway layer. The most common case in enterprise environments is REST-to-SOAP, where a new REST API sits in front of a legacy SOAP backend.&lt;/p&gt;

&lt;p&gt;Azure APIM handles this natively with the &lt;code&gt;soap-to-rest&lt;/code&gt; policy. On AWS or GCP, you write a Lambda or Cloud Run adapter that performs the translation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Azure APIM: expose a REST endpoint backed by a SOAP service --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;policies&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;inbound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Convert REST JSON body to SOAP XML envelope --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;set-body&amp;gt;&lt;/span&gt;
      @{
        var body = context.Request.Body.As&lt;span class="nt"&gt;&amp;lt;JObject&amp;gt;&lt;/span&gt;();
        return string.Format(
          @"&lt;span class="nt"&gt;&amp;lt;soapenv:Envelope&lt;/span&gt; &lt;span class="na"&gt;xmlns:soapenv=&lt;/span&gt;&lt;span class="s"&gt;'http://schemas.xmlsoap.org/soap/envelope/'&lt;/span&gt;
                              &lt;span class="na"&gt;xmlns:usr=&lt;/span&gt;&lt;span class="s"&gt;'http://legacy.example.com/user'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;soapenv:Body&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;usr:GetUser&amp;gt;&lt;/span&gt;
                  &lt;span class="nt"&gt;&amp;lt;usr:UserId&amp;gt;&lt;/span&gt;{0}&lt;span class="nt"&gt;&amp;lt;/usr:UserId&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;/usr:GetUser&amp;gt;&lt;/span&gt;
              &lt;span class="nt"&gt;&amp;lt;/soapenv:Body&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/soapenv:Envelope&amp;gt;&lt;/span&gt;",
          body["userId"]
        );
      }
    &lt;span class="nt"&gt;&amp;lt;/set-body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"Content-Type"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;text/xml; charset=utf-8&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;set-header&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"SOAPAction"&lt;/span&gt; &lt;span class="na"&gt;exists-action=&lt;/span&gt;&lt;span class="s"&gt;"override"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;value&amp;gt;&lt;/span&gt;http://legacy.example.com/user/GetUser&lt;span class="nt"&gt;&amp;lt;/value&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/set-header&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/inbound&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;backend&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/backend&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;outbound&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;base&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Parse SOAP response and return JSON --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;xml-to-json&lt;/span&gt; &lt;span class="na"&gt;kind=&lt;/span&gt;&lt;span class="s"&gt;"direct"&lt;/span&gt; &lt;span class="na"&gt;apply=&lt;/span&gt;&lt;span class="s"&gt;"always"&lt;/span&gt; &lt;span class="na"&gt;consider-accept-header=&lt;/span&gt;&lt;span class="s"&gt;"false"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/outbound&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/policies&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Protocol translation is one of the strongest arguments for Azure APIM. Implementing the same pattern on AWS requires a Lambda adapter and custom XML parsing logic. If your organization has legacy SOAP services that need a REST facade, I would pick Azure APIM over any other provider specifically for this — the AWS alternative is a Lambda that becomes a maintenance liability the moment the original SOAP developer leaves.&lt;/p&gt;




&lt;h2&gt;
  
  
  API Lifecycle Management
&lt;/h2&gt;

&lt;p&gt;Every gateway manages API versions moving through environments. The state diagram below represents the lifecycle that applies regardless of provider.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmchmulyiuz02egzpv1d2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmchmulyiuz02egzpv1d2.png" alt="API Lifecycle Management" width="464" height="1568"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: API lifecycle from development through retirement. Breaking changes are only safe before the API reaches Production.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On AWS, implement environment promotion by deploying to separate API Gateway stages (&lt;code&gt;dev&lt;/code&gt;, &lt;code&gt;staging&lt;/code&gt;, &lt;code&gt;prod&lt;/code&gt;). On Azure, use APIM's revision system for non-breaking changes and a new API version for breaking changes. On GCP API Gateway, deploy to separate gateway instances per environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing the Right Gateway
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpg7uq8ndlr168oro2zou.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpg7uq8ndlr168oro2zou.png" alt="Choosing the Right Gateway" width="800" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: Decision flowchart for selecting a gateway.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1: Using AWS REST API when HTTP API covers your needs&lt;/strong&gt;&lt;br&gt;
REST API costs 3.5× more per million requests and adds ~5 ms of latency over HTTP API. The only reasons to choose REST API over HTTP API are mapping templates for request/response transformation, usage plans with API keys, or Cognito user pool authorizers. If none of those apply, HTTP API is the correct choice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2: Ignoring Lambda cold starts in latency budgets&lt;/strong&gt;&lt;br&gt;
Teams measure gateway latency and see 6 ms. They assume their API responds in under 100 ms. Then they deploy to production and discover p99 latency is 700 ms because the Lambda behind the gateway cold-starts on every new burst. Cold starts are not a gateway problem — they are a Lambda problem — but they manifest as gateway latency and are often debugged at the wrong layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3: Choosing Azure APIM Consumption for production without reading the tier limitations&lt;/strong&gt;&lt;br&gt;
The Consumption tier has no SLA for the developer portal, limits on policy features, and no built-in cache. Teams select it for cost reasons and hit limitations mid-project. Standard is the correct production tier for most workloads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 4: Mixing multiple APIs in a single GCP project&lt;/strong&gt;&lt;br&gt;
GCP API Gateway enforces quotas at the project level. Multiple APIs in one project share quota. A traffic spike on one API reduces the available quota for all others in the same project. Separate projects per environment (and per service in production for high-traffic APIs) is the safe default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 5: Building complex API aggregation in gateway mapping templates&lt;/strong&gt;&lt;br&gt;
AWS REST API mapping templates (Velocity Template Language) and Azure APIM policies can both perform data transformation. Use them for simple field renaming or header injection. Do not use them to aggregate multiple backend calls, implement business logic, or transform complex nested structures — that complexity belongs in a BFF service where it is testable and maintainable.&lt;/p&gt;


&lt;h2&gt;
  
  
  Production Considerations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS HTTP API adds ~6 ms of median gateway overhead. REST API adds ~11 ms. Set your latency budget accordingly.&lt;/li&gt;
&lt;li&gt;Azure APIM Standard tier median overhead is 5–15 ms depending on policy complexity. A policy chain with JWT validation, rate limiting, and transformation adds up.&lt;/li&gt;
&lt;li&gt;GCP API Gateway overhead is 5–20 ms. Apigee overhead is higher due to the full policy engine.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Security&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On AWS, attach a WAF WebACL to your HTTP API or REST API. The gateway itself validates JWT signatures but does not inspect payloads for SQL injection or XSS patterns.&lt;/li&gt;
&lt;li&gt;On Azure APIM, use the &lt;code&gt;ip-filter&lt;/code&gt; policy to block known-bad IP ranges and the &lt;code&gt;validate-content&lt;/code&gt; policy for schema validation. APIM integrates with Azure DDoS Protection at the Premium tier.&lt;/li&gt;
&lt;li&gt;On GCP, pair API Gateway with Cloud Armor for DDoS protection and request filtering. Apigee has built-in bot detection and threat protection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cost&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS HTTP API at 1 billion requests/month: ~$1,000. Lambda execution costs are separate.&lt;/li&gt;
&lt;li&gt;Azure APIM Standard: ~$750/month flat plus overage. Standard includes 1M calls/month; additional calls are $3.50/million.&lt;/li&gt;
&lt;li&gt;GCP API Gateway at 1 billion requests/month: ~$2,000 (calls over the free tier at $3.00/million above 2M).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Monitoring&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On AWS, enable CloudWatch detailed metrics for your API Gateway and set alarms on &lt;code&gt;4XXError&lt;/code&gt;, &lt;code&gt;5XXError&lt;/code&gt;, &lt;code&gt;Latency&lt;/code&gt;, and &lt;code&gt;IntegrationLatency&lt;/code&gt;. &lt;code&gt;IntegrationLatency&lt;/code&gt; isolates backend latency from gateway latency — critical for cold start diagnosis.&lt;/li&gt;
&lt;li&gt;On Azure, use APIM's built-in Application Insights integration. Track &lt;code&gt;Backend Duration&lt;/code&gt; separately from &lt;code&gt;Gateway Duration&lt;/code&gt; to identify whether slowness is in the gateway policies or the backend.&lt;/li&gt;
&lt;li&gt;On GCP, Cloud Logging and Cloud Trace integrate automatically. Set a budget alert on API call costs in the GCP Console — GCP quotas can run up quickly on traffic spikes.&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Full Example: API Aggregation Service (TypeScript)
&lt;/h2&gt;

&lt;p&gt;This aggregation service sits behind an API Gateway (on any provider) and fans out to three downstream services in parallel. It returns a merged response or reports which upstream failed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;NextFunction&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;randomUUID&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// ─── Types ───────────────────────────────────────────────────────────────────&lt;/span&gt;

&lt;span class="cm"&gt;/** Represents the shape of a downstream service response for an order. */&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;OrderData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;total&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;lineItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;qty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/** Inventory availability per product. */&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;InventoryData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;available&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;reserved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/** Shipment tracking details. */&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ShipmentData&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;trackingNumber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;carrier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;estimatedDelivery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/** Merged summary returned to the client. */&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;OrderSummaryResponse&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrderData&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;InventoryData&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ShipmentData&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/** RFC 7807 problem detail shape for error responses. */&lt;/span&gt;
&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ProblemDetail&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ─── Helpers ─────────────────────────────────────────────────────────────────&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProblemDetail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://api.example.com/errors/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;...(&lt;/span&gt;&lt;span class="nx"&gt;instance&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}),&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/problem+json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Fetch a downstream service and parse the JSON response.
 *
 * @remarks Throws with a structured error if the upstream returns non-2xx.
 * Caller is responsible for catching via Promise.allSettled.
 *
 * @param url - Full URL of the downstream service endpoint.
 * @returns Parsed JSON response body.
 * @throws {Error} when upstream returns a non-2xx status.
 */&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fetchUpstream&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;X-Request-Id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// 5 s timeout per upstream call&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Upstream &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; returned &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// ─── Routes ──────────────────────────────────────────────────────────────────&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * GET /orders/:id/summary — fan-out aggregation across order, inventory, and shipping services.
 *
 * @remarks
 * **Path parameters:**
 * - `id` — Order UUID. Must exist in the order service.
 *
 * **Responses:**
 * - `200 OK` — All three upstreams responded successfully.
 * - `404 Not Found` — Order service returned 404.
 * - `502 Bad Gateway` — One or more upstreams failed or timed out; body identifies which.
 */&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/orders/:id/summary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requestId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ORDER_SVC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ORDER_SERVICE_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://order-service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;INV_SVC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;INVENTORY_SERVICE_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://inventory-service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SHIP_SVC&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SHIPPING_SERVICE_URL&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://shipping-service&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;orderResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;shipmentResult&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;fetchUpstream&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;OrderData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;ORDER_SVC&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/orders/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nx"&gt;fetchUpstream&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ShipmentData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;SHIP_SVC&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/shipments/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Upstream Failure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`Order service failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orderResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;orderResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Fetch inventory for each line item in parallel&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inventoryResults&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;allSettled&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lineItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nx"&gt;fetchUpstream&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;InventoryData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;INV_SVC&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/stock/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;productId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inventoryFailure&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inventoryResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rejected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;inventoryFailure&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Upstream Failure&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Inventory service failed for one or more products&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;OrderSummaryResponse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;requestId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;inventory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;inventoryResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;PromiseFulfilledResult&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;InventoryData&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;shipment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;shipmentResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fulfilled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;shipmentResult&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ─── Error handler ───────────────────────────────────────────────────────────&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NextFunction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unhandled error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nf"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Internal Server Error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;An unexpected error occurred.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ─── Start ───────────────────────────────────────────────────────────────────&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Aggregation service running on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
&lt;/span&gt;&lt;span class="nv"&gt;ORDER_SERVICE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:3001 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nv"&gt;INVENTORY_SERVICE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:3002 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nv"&gt;SHIPPING_SERVICE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:3003 &lt;span class="se"&gt;\&lt;/span&gt;
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test the aggregation endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Aggregated response from three services in one call&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://localhost:3000/orders/order-abc123/summary | jq

&lt;span class="c"&gt;# Expected shape:&lt;/span&gt;
&lt;span class="c"&gt;# {&lt;/span&gt;
&lt;span class="c"&gt;#   "requestId": "uuid",&lt;/span&gt;
&lt;span class="c"&gt;#   "order": { "id": "order-abc123", "status": "confirmed", ... },&lt;/span&gt;
&lt;span class="c"&gt;#   "inventory": [{ "productId": "...", "available": 10 }, ...],&lt;/span&gt;
&lt;span class="c"&gt;#   "shipment": { "trackingNumber": "...", "status": "in_transit" }&lt;/span&gt;
&lt;span class="c"&gt;# }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Full source with tests: &lt;a href="https://github.com/brywritescode/bry-writes-code-examples.git" rel="noopener noreferrer"&gt;GitHub link&lt;/a&gt; → &lt;code&gt;cloud-apis/api-gateway-patterns/&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;AWS HTTP API is the right default for Lambda-backed services — it is faster and costs less than REST API, and covers 90% of use cases. Azure APIM wins when your organization needs a developer portal, enterprise API governance, or REST-to-SOAP translation without custom code. GCP's three options serve different tiers: API Gateway for serverless, Cloud Endpoints for gRPC, and Apigee for enterprise. The BFF pattern, API aggregation, and protocol translation are cloud-agnostic in concept but differ significantly in implementation cost — Azure APIM handles protocol translation natively, while AWS and GCP require adapter services. The decision you should walk away from this article having made: stop treating gateway selection as a checkbox in your cloud provider's onboarding wizard. The wrong choice costs you months of workarounds; the right one disappears into your infrastructure and you stop thinking about it entirely — which is exactly what a good gateway should do.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html" rel="noopener noreferrer"&gt;AWS API Gateway Developer Guide — HTTP API vs REST API&lt;/a&gt; — official AWS documentation on choosing between the two products&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://learn.microsoft.com/en-us/azure/api-management/" rel="noopener noreferrer"&gt;Azure API Management documentation&lt;/a&gt; — official Microsoft docs covering policies, tiers, and the developer portal&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/blog/products/application-modernization/choosing-between-apigee-api-gateway-and-cloud-endpoints" rel="noopener noreferrer"&gt;Choosing between Apigee, API Gateway, and Cloud Endpoints — Google Cloud Blog&lt;/a&gt; — official Google guidance on selecting the right GCP product&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/api-gateway/docs" rel="noopener noreferrer"&gt;GCP API Gateway documentation&lt;/a&gt; — OpenAPI configuration, authentication, and quota management&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bry Writes Code — cloud and API infrastructure specialist. Evaluating or designing your API gateway architecture? &lt;a href="mailto:brywritescode@gmail.com"&gt;Get in touch&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>aws</category>
      <category>typescript</category>
      <category>backend</category>
    </item>
    <item>
      <title>Story Points After AI: When Velocity Metrics Stop Meaning What They Used To</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Wed, 12 Aug 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/story-points-after-ai-when-velocity-metrics-stop-meaning-what-they-used-to-1jkf</link>
      <guid>https://dev.to/brywritescode/story-points-after-ai-when-velocity-metrics-stop-meaning-what-they-used-to-1jkf</guid>
      <description>&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Story points and velocity were built on an assumption: that the effort to draft a solution roughly tracked the effort to deliver it. AI is breaking that link by collapsing drafting effort toward zero while leaving verification effort largely where it was.&lt;/li&gt;
&lt;li&gt;A team whose velocity jumps from 50 to 5,000 in a sprint hasn't gotten 100x better. It's produced a number that no longer measures the thing anyone cares about, and more engineering leaders are quietly admitting this every quarter.&lt;/li&gt;
&lt;li&gt;What's replacing velocity isn't a single metric. It's a shift toward outcome-oriented measures: defect density in AI-generated code, rework percentage, lead time to value, and issue resolution time, none of which can be inflated just by generating more draft code faster.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://medium.com/@brywritescode/why-the-man-month-is-dying-how-ai-broke-it-services-oldest-pricing-unit-d8121156c937" rel="noopener noreferrer"&gt;Why the Man-Month Is Dying: How AI Broke IT Services' Oldest Pricing Unit&lt;/a&gt; covered why the man-month is dying as a pricing unit. This one covers the parallel death of story points as a delivery-measurement unit. The two are breaking for the same underlying reason, on different sides of the same contract.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;A client-side engineering director told me recently that her team's velocity chart looked like a hockey stick after they rolled out agentic coding tools, and that she'd stopped showing it to her own leadership because it made her look like either a liar or an idiot. Neither was true. Her team had actually improved. The chart just couldn't say by how much, or at what, because the number it produced no longer meant what it used to mean.&lt;/p&gt;

&lt;p&gt;Story points measure estimated effort, and velocity is story points delivered per sprint, a proxy that worked reasonably well as long as the bottleneck in software delivery was drafting: writing the code, wiring up the boilerplate, working through the mechanical parts of a ticket. AI-assisted development attacks exactly that bottleneck and leaves the other one, verification, largely untouched. When drafting effort collapses toward zero and verification effort doesn't move much, a velocity number built on the old ratio between the two stops correlating with anything real. Scrum.org's own framing of the problem is blunt: if a team's velocity jumps from 50 to 5,000 in a single sprint, they haven't gotten 100x better. They've broken the metric, because it was always a proxy for effort, and AI reduces the drafting effort to near-zero while leaving verification complexity high.&lt;/p&gt;

&lt;p&gt;The industry data backs up how fast this is becoming unignorable. Teams running agentic AI tools report 10-20x velocity gains alongside 40-78% fewer bugs, numbers that sound like unambiguous good news until you try to use them for the same planning and comparison purposes velocity was always used for. You cannot forecast a roadmap, compare two teams, or set a hiring plan against a metric that swings by two orders of magnitude depending on which AI tool got rolled out last quarter. Teams that haven't found a replacement are flying blind with better instruments than they've ever had, which is its own kind of dangerous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Old Velocity Metrics vs. Post-AI Delivery Metrics
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criteria&lt;/th&gt;
&lt;th&gt;Story Points / Velocity&lt;/th&gt;
&lt;th&gt;Post-AI Delivery Metrics&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What it actually measures&lt;/td&gt;
&lt;td&gt;Estimated drafting effort per sprint&lt;/td&gt;
&lt;td&gt;Defect density, rework rate, lead time to value, issue resolution time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inflatable by AI tooling alone&lt;/td&gt;
&lt;td&gt;Yes, trivially, without any real gain&lt;/td&gt;
&lt;td&gt;No, each metric requires an actual outcome to move&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Useful for cross-team comparison&lt;/td&gt;
&lt;td&gt;Only if both teams use identical tooling and estimation habits&lt;/td&gt;
&lt;td&gt;Yes, outcomes are tooling-agnostic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Useful for client-facing reporting&lt;/td&gt;
&lt;td&gt;Increasingly not, since AI adoption accelerated in 2026&lt;/td&gt;
&lt;td&gt;Yes, clients can verify a defect rate or a resolution time against their own experience&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What a spike in the number tells you&lt;/td&gt;
&lt;td&gt;Nothing reliable&lt;/td&gt;
&lt;td&gt;That verification-adjusted output actually improved&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Recommendation:&lt;/strong&gt; if your organization is still running sprint retros around a velocity chart, check whether anyone on the team can explain what a 10x jump in that number would actually mean. If the honest answer is "the tooling changed," the metric isn't tracking delivery anymore. It's tracking tool adoption, and you should be measuring that separately from outcome quality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where This Is Likely Headed
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Right now: denial and dashboards for most teams.&lt;/strong&gt; Most teams are keeping the velocity chart because the tooling to replace it (defect-density tracking, rework attribution, lead-time-to-value instrumentation) isn't built yet, and nobody wants to be first to admit their headline metric is cosmetic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expect a credibility break within the next year or two.&lt;/strong&gt; As more teams get publicly embarrassed by an unexplainable 50-to-5,000 velocity swing, engineering leadership will stop defending the metric in front of executives and clients. "Agent efficiency" language is already starting to replace "velocity" in serious planning conversations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outcome metrics should go from experimental to standard within a few years.&lt;/strong&gt; Rework percentage and AI-attributed defect rate are becoming normal line items on engineering dashboards, not because they're perfect, but because they can't be gamed by pointing an agent at a backlog overnight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The end state: metrics become a client-facing artifact, not just an internal one.&lt;/strong&gt; Firms moving toward outcome-based contracts are starting to share rework and defect data directly with clients, because the metric is finally honest enough to survive that exposure.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Questions to Ask Your Team
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If our velocity doubled next sprint, could anyone on this team explain exactly why, in terms a client would find credible?&lt;/li&gt;
&lt;li&gt;Are we tracking rework and defect rate on AI-generated code specifically, or lumping it in with everything else and losing the signal?&lt;/li&gt;
&lt;li&gt;Would we be comfortable sharing our current delivery metrics directly with a client, or are they really an internal-only number dressed up as evidence of progress?&lt;/li&gt;
&lt;li&gt;Is any part of a team's evaluation or bonus still tied to raw velocity, in a way that quietly rewards inflating a number everyone privately agrees is broken?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Story points and velocity aren't failing because someone calculated them wrong. They're failing because the thing they measured, effort, is no longer correlating with the thing everyone actually wanted to know, which was progress. The teams adapting fastest aren't the ones with the flashiest AI tooling. They're the ones willing to admit their dashboard is lying to them and replace it with metrics, defect density, rework rate, lead time to value, that can't be inflated by the same tools that broke the old ones. The man-month is dying on the pricing side of the contract. Velocity is dying on the delivery side. Neither death is really about AI being too good. It's about a proxy finally getting exposed as a proxy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.scrum.org/resources/blog/velocity-agent-efficiency-evidence-based-management-ai-era" rel="noopener noreferrer"&gt;Scrum.org: From Velocity to "Agent Efficiency", Evidence-Based Management for the AI Era&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://oobeya.io/blog/engineering-metrics-in-the-ai-era" rel="noopener noreferrer"&gt;Oobeya: Engineering Metrics in the AI Era, A Complete Guide for 2026&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gitvelocity.dev/blog/measuring-engineering-ai-era" rel="noopener noreferrer"&gt;GitVelocity: AI Broke Your Engineering Metrics. Here's What Works Now&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bry Writes Code; cloud and AI infrastructure specialist. Still reporting velocity to a client and not sure it means anything anymore? &lt;a href="mailto:brywritescode@gmail.com"&gt;Let's talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>business</category>
      <category>ai</category>
      <category>agile</category>
      <category>beginners</category>
    </item>
    <item>
      <title>AWS EventBridge: Event Buses and Rules You Can Script from Day One</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Tue, 11 Aug 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/aws-eventbridge-event-buses-and-rules-you-can-script-from-day-one-106f</link>
      <guid>https://dev.to/brywritescode/aws-eventbridge-event-buses-and-rules-you-can-script-from-day-one-106f</guid>
      <description>&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;EventBridge charges $1.00 per million custom events published; rule evaluation itself is free, up to 300 rules per bus. The real billing surprise isn't rule count — it's payload size, since events bill in 64 KB chunks and the January 2026 limit increase to 1 MB means one large event can now bill as up to 16.&lt;/li&gt;
&lt;li&gt;A custom event bus, a rule with a JSON event pattern, and a Lambda target take four CLI commands total. Use &lt;code&gt;InputTransformer&lt;/code&gt; on targets to trim events down to only the fields a target needs, rather than forwarding the full body downstream.&lt;/li&gt;
&lt;li&gt;Archive and replay are cheap to enable and expensive to forget about — archived data bills monthly storage whether or not you ever replay it.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CLI/SDK version tested against: &lt;code&gt;aws-cli/2.35.x&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;An IAM role with &lt;code&gt;events:*&lt;/code&gt; and &lt;code&gt;lambda:InvokeFunction&lt;/code&gt; permissions&lt;/li&gt;
&lt;li&gt;A deployed Lambda function to act as a target — the examples use &lt;code&gt;order-processor&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;EventBridge gets pitched as "SNS but smarter" often enough that people underrate how little code it takes to get real value from it. I've stood up event-driven order processing for two different clients using nothing but a custom bus, a handful of pattern-matched rules, and existing Lambda functions — no message broker to run, no queue infrastructure to patch.&lt;/p&gt;

&lt;p&gt;The part that doesn't show up in the marketing is the cost mechanic that changed in January 2026: AWS raised the maximum event payload from 256 KB to 1 MB, which sounds like a pure win until you notice events still bill in 64 KB chunks. A single 1 MB event can now bill as 16 separate chunked events at the custom-event rate. That's not a reason to avoid EventBridge — it's a reason to design your event payloads deliberately instead of dumping an entire domain object into &lt;code&gt;detail&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This article builds a working bus, rule, and target from the terminal, and treats the payload mechanic as a design input, not an afterthought.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bus, Rules, Targets: The Three Pieces
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7br94gvx2akygpucqlo0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7br94gvx2akygpucqlo0.png" alt="Bus, Rules, Targets: The Three Pieces" width="784" height="163"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: one bus, multiple rules each matching a different event pattern, each routing to its own target.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A rule without a target does nothing but sit there for free. A rule with a matching event pattern and a wired-up target is what actually moves data. Rules and targets are separate API calls on purpose — one rule can fan out to up to five targets.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building It From the CLI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. A custom bus — separate from the default bus so your rules&lt;/span&gt;
&lt;span class="c"&gt;#    only ever see events your own services publish.&lt;/span&gt;
aws events create-event-bus &lt;span class="nt"&gt;--name&lt;/span&gt; orders-bus

&lt;span class="c"&gt;# 2. A rule matching a specific event pattern on that bus.&lt;/span&gt;
aws events put-rule &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; order-created-rule &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--event-bus-name&lt;/span&gt; orders-bus &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--event-pattern&lt;/span&gt; &lt;span class="s1"&gt;'{
    "source": ["orders-service"],
    "detail-type": ["OrderCreated"],
    "detail": {
      "status": ["pending"]
    }
  }'&lt;/span&gt;

&lt;span class="c"&gt;# 3. Grant the rule permission to invoke the Lambda target.&lt;/span&gt;
aws lambda add-permission &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; order-processor &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--statement-id&lt;/span&gt; eventbridge-invoke &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--action&lt;/span&gt; lambda:InvokeFunction &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--principal&lt;/span&gt; events.amazonaws.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source-arn&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:events:us-east-1:123456789012:rule/orders-bus/order-created-rule"&lt;/span&gt;

&lt;span class="c"&gt;# 4. Wire the rule to the Lambda target.&lt;/span&gt;
aws events put-targets &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--rule&lt;/span&gt; order-created-rule &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--event-bus-name&lt;/span&gt; orders-bus &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--targets&lt;/span&gt; &lt;span class="s1"&gt;'[{
    "Id": "order-processor-target",
    "Arn": "arn:aws:lambda:us-east-1:123456789012:function:order-processor"
  }]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four commands, and the bus is live. Publish a test event to confirm the wiring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws events put-events &lt;span class="nt"&gt;--entries&lt;/span&gt; &lt;span class="s1"&gt;'[{
  "Source": "orders-service",
  "DetailType": "OrderCreated",
  "Detail": "{\"orderId\":\"order-abc123\",\"status\":\"pending\",\"total\":49.99}",
  "EventBusName": "orders-bus"
}]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A successful response returns &lt;code&gt;"FailedEntryCount": 0&lt;/code&gt;. If it's &lt;code&gt;1&lt;/code&gt;, check &lt;code&gt;Errors&lt;/code&gt; in the same response — the two most common causes are a malformed &lt;code&gt;Detail&lt;/code&gt; JSON string or a &lt;code&gt;detail-type&lt;/code&gt;/&lt;code&gt;source&lt;/code&gt; combination that doesn't match any rule, which fails silently rather than erroring (the event is simply dropped with no matching rule, and EventBridge does not treat that as a failure).&lt;/p&gt;




&lt;h2&gt;
  
  
  Designing Around the Payload-Chunking Rule
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp4wwnkw3c7c9x2y98vcf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp4wwnkw3c7c9x2y98vcf.png" alt="Designing Around the Payload-Chunking Rule" width="494" height="547"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: EventBridge's per-64KB chunking mechanic, effective with the January 2026 payload limit increase to 1 MB.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The fix isn't "never send large events." It's "don't send more than the target needs." Two concrete patterns:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Send a reference, not the full object.&lt;/strong&gt; If &lt;code&gt;order-created-rule&lt;/code&gt;'s target only needs the order ID to go fetch full details itself, publish &lt;code&gt;{"orderId": "order-abc123"}&lt;/code&gt; in &lt;code&gt;detail&lt;/code&gt;, not the entire order document with line items, customer data, and shipping addresses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;InputTransformer&lt;/code&gt; to trim before delivery.&lt;/strong&gt; When the target genuinely needs specific fields from a larger event, extract them at the rule level instead of forwarding everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws events put-targets &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--rule&lt;/span&gt; order-created-rule &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--event-bus-name&lt;/span&gt; orders-bus &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--targets&lt;/span&gt; &lt;span class="s1"&gt;'[{
    "Id": "order-processor-target",
    "Arn": "arn:aws:lambda:us-east-1:123456789012:function:order-processor",
    "InputTransformer": {
      "InputPathsMap": {
        "orderId": "$.detail.orderId",
        "status": "$.detail.status"
      },
      "InputTemplate": "{\"orderId\": &amp;lt;orderId&amp;gt;, \"status\": &amp;lt;status&amp;gt;}"
    }
  }]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;InputTransformer&lt;/code&gt; runs on EventBridge's side before delivery — it doesn't reduce what you're billed for publishing the original event, but it does mean your Lambda isn't parsing (and your logs aren't storing) a payload larger than it needs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Archive and Replay
&lt;/h2&gt;

&lt;p&gt;Archiving lets you replay events later — useful for reprocessing after a bug fix, or reconstructing state after a downstream outage. It's cheap per operation and easy to forget about entirely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws events create-archive &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--archive-name&lt;/span&gt; orders-archive &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--event-source-arn&lt;/span&gt; arn:aws:events:us-east-1:123456789012:event-bus/orders-bus &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--retention-days&lt;/span&gt; 30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Price&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Archive processing&lt;/td&gt;
&lt;td&gt;$0.10/GB archived&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Archive storage&lt;/td&gt;
&lt;td&gt;$0.023/GB-month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replayed events&lt;/td&gt;
&lt;td&gt;$1.00/million (same as custom event rate)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Storage bills every month an archive exists, whether or not anything ever gets replayed from it. Set &lt;code&gt;--retention-days&lt;/code&gt; deliberately — 30 days is a reasonable default for operational replay; longer retention should be a conscious compliance or audit decision, not the default you forgot to change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1: Publishing to the default bus for everything&lt;/strong&gt;&lt;br&gt;
The default bus receives AWS service events too. Mixing your application events into it makes rule patterns harder to write precisely and makes the bus noisier to reason about. Use a custom bus per domain area.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2: Assuming a failed match throws an error&lt;/strong&gt;&lt;br&gt;
It doesn't. An event with no matching rule is dropped silently — &lt;code&gt;put-events&lt;/code&gt; still returns success. Test your event patterns against real sample events before relying on them in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3: Forwarding entire event payloads to every target&lt;/strong&gt;&lt;br&gt;
This is both a cost issue (larger payloads, more chunked billing on any downstream re-publish) and a coupling issue — targets that receive more than they need tend to accumulate implicit dependencies on fields nobody documented.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 4: Enabling archive without a retention policy decision&lt;/strong&gt;&lt;br&gt;
Storage costs accrue every month regardless of replay activity. An archive with no retention limit and no owner is exactly the kind of AWS bill line item nobody can explain six months later.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production Considerations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Performance:&lt;/strong&gt; Rule matching is fast and free regardless of rule count up to the 300-per-bus limit. If you're approaching that limit, it's usually a sign you need multiple buses segmented by domain, not one bus straining to model everything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security:&lt;/strong&gt; Use resource-based policies on the event bus (&lt;code&gt;put-permission&lt;/code&gt;) to control which accounts or services can publish, rather than relying solely on IAM policies at the producer side.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; Re-run the payload math whenever a producer's event shape grows. A field added "just in case" on a high-volume event source compounds fast at the chunked billing rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitoring:&lt;/strong&gt; CloudWatch metrics on &lt;code&gt;TriggeredRules&lt;/code&gt;, &lt;code&gt;FailedInvocations&lt;/code&gt;, and &lt;code&gt;ThrottledRules&lt;/code&gt; are free and on by default — set an alarm on &lt;code&gt;FailedInvocations&lt;/code&gt; per target, since a misconfigured target fails silently from the publisher's perspective.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full Example: Teardown Script
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;BUS_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;BUS_NAME&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;-bus&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;RULE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RULE_NAME&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;order&lt;/span&gt;&lt;span class="p"&gt;-created-rule&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

aws events remove-targets &lt;span class="nt"&gt;--rule&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RULE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--event-bus-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BUS_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--ids&lt;/span&gt; order-processor-target
aws events delete-rule &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RULE_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--event-bus-name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BUS_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
aws events delete-archive &lt;span class="nt"&gt;--archive-name&lt;/span&gt; orders-archive &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
&lt;/span&gt;aws events delete-event-bus &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BUS_NAME&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Torn down &lt;/span&gt;&lt;span class="nv"&gt;$BUS_NAME&lt;/span&gt;&lt;span class="s2"&gt; and its rules."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Full source including the sample Lambda target and pattern-matching tests: &lt;a href="https://github.com/brywritescode/bry-writes-code-examples.git" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; → &lt;code&gt;cloud-apis/aws-eventbridge-cli/&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;EventBridge is one of the fastest AWS services to script from zero — a working bus, rule, and target is four commands. The part worth designing around deliberately is payload size: with the 2026 increase to a 1 MB event limit, it's easy to publish something large enough to bill as multiple events without noticing. Send references and trim payloads with &lt;code&gt;InputTransformer&lt;/code&gt; rather than forwarding full objects downstream, and treat archive retention as a decision you make on purpose, not a default you forgot existed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/eventbridge/pricing/" rel="noopener noreferrer"&gt;Amazon EventBridge pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cli/latest/reference/events/" rel="noopener noreferrer"&gt;events — AWS CLI Command Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-targets.html" rel="noopener noreferrer"&gt;Event bus targets in Amazon EventBridge&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://repost.aws/knowledge-center/eventbridge-reduce-charges" rel="noopener noreferrer"&gt;Understand EventBridge charges and reduce future charges — AWS re:Post&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bry Writes Code — cloud and API infrastructure specialist. Designing an event-driven architecture on AWS? &lt;a href="mailto:brywritescode@gmail.com"&gt;Get in touch&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>aws</category>
      <category>typescript</category>
      <category>backend</category>
    </item>
    <item>
      <title>Why the Man-Month Is Dying: How AI Broke IT Services' Oldest Pricing Unit</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Wed, 05 Aug 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/why-the-man-month-is-dying-how-ai-broke-it-services-oldest-pricing-unit-3dop</link>
      <guid>https://dev.to/brywritescode/why-the-man-month-is-dying-how-ai-broke-it-services-oldest-pricing-unit-3dop</guid>
      <description>&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The man-month (one person, one month of billable effort, priced roughly the same regardless of what actually got produced) was the pricing atom of IT services for over several decades ago. By 2026 it's a legacy unit, still written into a shrinking pool of contracts nobody has gotten around to renegotiating.&lt;/li&gt;
&lt;li&gt;The break point is dateable. In February 2026, Fujitsu publicly attributed a modification job that would have taken three man-months to four hours of AI-assisted work, and formally announced a shift "from the conventional man-month base to a customer-provided value base." NTT Data and INSEAD's joint research that April reached the same conclusion from the outside: the man-month model's sustainability was no longer assumed. It was in question.&lt;/li&gt;
&lt;li&gt;This opens an eight-part thread on what replaced the man-month across systems integration: pricing, subcontracting, staffing, delivery practice. It starts here, with the unit itself and why it broke.&lt;/li&gt;
&lt;li&gt;What replaced it isn't one model. Outcome-based fees, asset/subscription hybrids, and old-fashioned time-and-materials all still exist, but they're chosen deliberately now, not defaulted to because nobody bothered to price the actual result.&lt;/li&gt;
&lt;li&gt;The firms that struggled hardest weren't the ones using AI badly. They were the ones whose entire commercial structure, quotas, career ladders, partner compensation, was load-bearing on selling hours, with nothing else to sell once hours stopped being the scarce resource.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I sat through a renegotiation meeting in late 2026 where a client's procurement lead asked our account team, flatly: "You're telling me this now takes a quarter of the time it used to. Why does the invoice look the same?" Nobody in the room had a good answer. The honest one, "our pricing model was never actually about the work, it was about the hours we could bill for it," isn't something you say out loud to a client who just found that out for themselves.&lt;/p&gt;

&lt;p&gt;That meeting was a preview of what happened to the entire industry over the years to come. The man-month, the unit IT services has priced against since the mainframe era, assumed labor was the scarce, expensive input and that more of it reliably meant more delivered value. AI-assisted development broke that assumption specifically, not generally. It didn't make engineers better at everything. It made the drafting and implementation portion of the work cheap enough that billing by the hour spent on it started to look less like a pricing model and more like an admission that nobody had measured what the client actually received.&lt;/p&gt;

&lt;p&gt;Fujitsu's February 2026 disclosure is the cleanest marker of when this became undeniable inside the industry rather than just argued about outside it: a modification job estimated at three man-months got done in four hours with AI assistance, and the company followed with an official policy statement moving away from man-month pricing toward pricing tied to customer value. NTT Data didn't wait to be embarrassed by a competitor's number. Its joint research with INSEAD two months later stated directly that AI adoption was affecting "the sustainability of the traditional man-month model," and recommended outcome-oriented or asset-utilization pricing instead. Two of Japan's largest systems integrators, independently, arrived at the same read within a single quarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Man-Month Billing vs. What Replaced It
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criteria&lt;/th&gt;
&lt;th&gt;Man-Month Billing&lt;/th&gt;
&lt;th&gt;Outcome / Asset-Based Billing&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What the client pays for&lt;/td&gt;
&lt;td&gt;Time occupied by a person, regardless of output&lt;/td&gt;
&lt;td&gt;A defined result, a usage tier, or an asset the vendor built once and licenses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vendor incentive under AI acceleration&lt;/td&gt;
&lt;td&gt;Perverse: faster delivery shrinks the invoice&lt;/td&gt;
&lt;td&gt;Aligned: faster delivery keeps the same fee and improves margin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client's ability to verify value received&lt;/td&gt;
&lt;td&gt;Weak. Trusts the vendor's time reporting&lt;/td&gt;
&lt;td&gt;Strong. Result is specified and checkable before payment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Effect of a 10x AI productivity gain&lt;/td&gt;
&lt;td&gt;Revenue collapses unless hours are padded&lt;/td&gt;
&lt;td&gt;Revenue holds; the gain becomes margin, not a threat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What a firm needs to sell it&lt;/td&gt;
&lt;td&gt;Headcount utilization&lt;/td&gt;
&lt;td&gt;A track record, a defined asset, or a measurable outcome definition&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Recommendation:&lt;/strong&gt; if your organization is still quoting new work by estimated person-months in 2033, that's not a pricing choice anymore. It's a sign nobody has built the measurement infrastructure (defined outcomes, reusable assets, usage metering) that every other model in this table requires. That infrastructure is the actual work of the transition, not the invoice template.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Transition is Expected to Unfolded, 2026 onwards
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;2026: acknowledgment, not action.&lt;/strong&gt; Fujitsu and NTT Data said the model was breaking. Almost nobody had a replacement ready. Outcome-based contracts existed in theory (IDC was already projecting 30% of IT services contracts would be outcome-based by 2029), but most firms kept quoting man-months out of habit and contract-template inertia.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The bifurcation.&lt;/strong&gt; Firms split into two groups. One group treated AI purely as a cost lever: same man-month invoices, fewer people doing the work, margin absorbed internally as long as clients didn't notice. The other group renegotiated openly, moved toward hybrid subscription-plus-usage or defined-outcome pricing, and used the freed-up delivery capacity to take on more client relationships instead of padding timesheets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forecasts beaten, not missed.&lt;/strong&gt; IDC's Forecast that the 30%-outcome-based-by-2029 number that looked aggressive in 2026 may turn out to be conservative. Clients who'd seen one vendor bill honestly for AI-accelerated work stopped tolerating man-month invoices from everyone else.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consolidation around the honest group.&lt;/strong&gt; The firms still running pure headcount-utilization economics didn't disappear overnight, but their new-business win rate kept sliding against competitors who could show a client exactly what they were paying for and why the price made sense at AI-native delivery speed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Questions to Ask Your Team
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;If we quoted this job today the way we would have in 2025, by estimated person-months, would the number even make sense against how fast we can actually deliver it now?&lt;/li&gt;
&lt;li&gt;Do we have a defined, checkable outcome for our top five client engagements, or are we still trusting time reports as the only measure of value delivered?&lt;/li&gt;
&lt;li&gt;When AI cuts delivery time on a piece of work, does that show up as margin for us or as an automatic discount we're not charging for?&lt;/li&gt;
&lt;li&gt;Is any part of our compensation or promotion structure still built around billable-hour targets that actively punish a team for delivering faster?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The man-month didn't die because AI made engineers obsolete. It died because it was always a proxy, for effort, for scarcity, for how much of a scarce input a client needed to buy, and AI broke the proxy's connection to the thing it was supposed to measure. Fujitsu and NTT Data said so publicly in 2026 because the gap had gotten too large to paper over. What comes next in this thread is the part that actually took years to work out: what replaced story points as a proxy for progress, how subcontracting economics reshuffled under the same pressure, and what a systems integrator actually sells once headcount stops being the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://note.com/zouplans/n/n8e3247ccdffc?hl=en" rel="noopener noreferrer"&gt;note.com: How Generative AI Will Change Japan's SI Industry (Spring 2026)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hig.com/news/it-services-in-the-age-of-agentic-ai-underwriting-through-a-structural-shift/" rel="noopener noreferrer"&gt;H.I.G. Capital: IT Services in the Age of Agentic AI, Underwriting Through a Structural Shift&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://activatesignal.substack.com/p/ai-isnt-killing-it-services" rel="noopener noreferrer"&gt;Activate Signal: AI Isn't Killing IT Services&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Bry Writes Code; cloud and AI infrastructure specialist. Renegotiating a services contract that hasn't caught up to what AI actually costs to deliver? &lt;a href="mailto:brywritescode@gmail.com"&gt;Let's talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>business</category>
      <category>ai</category>
      <category>consulting</category>
      <category>beginners</category>
    </item>
    <item>
      <title>API Gateway for Business Leaders: What It Is, Why It Matters, and How to Adopt It</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Tue, 04 Aug 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/api-gateway-for-business-leaders-what-it-is-why-it-matters-and-how-to-adopt-it-4bgm</link>
      <guid>https://dev.to/brywritescode/api-gateway-for-business-leaders-what-it-is-why-it-matters-and-how-to-adopt-it-4bgm</guid>
      <description>&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An API gateway is the single front door through which all digital traffic enters your software systems — like a hotel concierge that handles every request before routing it to the right department.&lt;/li&gt;
&lt;li&gt;Without one, every client application talks directly to every backend service — a security nightmare that gets worse as you scale.&lt;/li&gt;
&lt;li&gt;The core business payoff: one place to enforce security, one place to see all traffic, and the freedom to change your backend without disrupting your customers.&lt;/li&gt;
&lt;li&gt;Main costs are driven by API call volume and data transfer — budget $50–$500/month for small to mid-size deployments on managed cloud providers.&lt;/li&gt;
&lt;li&gt;Adopt if you run more than three backend services and serve more than one client application; skip it if you have a single small app with one consumer.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Your engineering team has built ten microservices over three years. Each one has its own authentication logic. None of them share a logging format. When a security vulnerability hits one, your team scrambles to patch ten places. When a new mobile app wants to access any of them, integration takes weeks per service.&lt;/p&gt;

&lt;p&gt;This is the API sprawl problem — and it gets worse every time your business adds a new product, partner integration, or user-facing application. The API management market reached $8.9 billion in 2024 and is growing at over 17% annually, driven by organizations recognizing that managing APIs at scale without a central control layer is unsustainable.&lt;/p&gt;

&lt;p&gt;An API gateway is that control layer. I've seen this exact problem in organizations that had six engineers, then twenty, then sixty — and the teams that centralized their API infrastructure early were the ones that didn't spend half their fourth year firefighting integration debt. This article explains what it does, what it costs, who should adopt it, and how to evaluate whether your organization is ready.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is an API Gateway?
&lt;/h2&gt;

&lt;p&gt;Think of an API gateway as the customs checkpoint at an international airport. Every passenger — every request from a mobile app, partner system, or internal tool — passes through a single checkpoint before reaching their destination. The checkpoint verifies identity, checks what you are allowed to carry through, routes you to the right terminal, and logs your passage. None of the individual terminals need to run their own security lines.&lt;/p&gt;

&lt;p&gt;Without a gateway, your software looks like an airport where every gate runs its own customs procedure. Passengers (requests) arrive at any door they can find. Each gate hires its own security staff. The records are fragmented. When a threat emerges, there is no single place to shut it down.&lt;/p&gt;

&lt;p&gt;In technical terms: an API gateway is a reverse proxy that sits between your clients and your backend services, handling authentication, routing, rate limiting, monitoring, and protocol translation centrally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem it solves:&lt;/strong&gt; as organizations add services, the number of direct connections between clients and services grows exponentially. Five services serving four client types means up to twenty separate integration points to secure, monitor, and maintain — each one a potential vulnerability and a maintenance burden.&lt;/p&gt;

&lt;h3&gt;
  
  
  Before and After
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Felvzx5tknua15e0x3yzh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Felvzx5tknua15e0x3yzh.png" alt="Before" width="800" height="771"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvi31xzhsig0zw7rppoox.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvi31xzhsig0zw7rppoox.png" alt="After" width="800" height="506"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Before: nine direct connections, each requiring its own security, logging, and maintenance. After: three connections to one gateway — which manages everything downstream.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Benefits
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Benefit&lt;/th&gt;
&lt;th&gt;What It Means in Practice&lt;/th&gt;
&lt;th&gt;Who Gains&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Centralized security&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Authentication and access control enforced once, at the gateway — not duplicated across every service&lt;/td&gt;
&lt;td&gt;Security team, compliance officers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Full traffic visibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Every API call is logged in one place — who called what, when, how often, whether it succeeded&lt;/td&gt;
&lt;td&gt;Engineering, product, finance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Backend flexibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Change or replace a backend service without touching client applications&lt;/td&gt;
&lt;td&gt;Engineering team, product velocity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rate limiting and abuse prevention&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automatically throttle runaway traffic before it reaches your systems&lt;/td&gt;
&lt;td&gt;Operations, infrastructure cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Faster partner integrations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;New partners connect once to the gateway, not to each individual service&lt;/td&gt;
&lt;td&gt;Business development, sales&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost predictability&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Caching at the gateway reduces redundant backend calls and cloud compute costs&lt;/td&gt;
&lt;td&gt;Finance, engineering&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  With Gateway vs Without Gateway
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criteria&lt;/th&gt;
&lt;th&gt;Without API Gateway&lt;/th&gt;
&lt;th&gt;With API Gateway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Security surface&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Distributed — each service is an independent attack target&lt;/td&gt;
&lt;td&gt;Centralized — one hardened entry point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Time to add a new client&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Weeks — each service must be integrated separately&lt;/td&gt;
&lt;td&gt;Days — connect once to the gateway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Incident response time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Slow — must patch auth or logging in multiple services&lt;/td&gt;
&lt;td&gt;Fast — single change propagates everywhere&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Traffic visibility&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fragmented — each service logs differently or not at all&lt;/td&gt;
&lt;td&gt;Unified — consistent logs, dashboards, alerts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ability to scale teams independently&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Low — teams depend on each other's service interfaces&lt;/td&gt;
&lt;td&gt;High — gateway contract isolates frontend and backend teams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Operational cost at scale&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High — duplicated effort across every service&lt;/td&gt;
&lt;td&gt;Lower — shared infrastructure amortized across all services&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vendor / tech migration risk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;High — clients are tightly coupled to backend implementation&lt;/td&gt;
&lt;td&gt;Low — swap backend services without client changes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Bottom line:&lt;/strong&gt; for organizations running more than three backend services or serving more than one client type, an API gateway pays for itself in reduced integration and security overhead within the first year.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Works (Without the Code)
&lt;/h2&gt;

&lt;p&gt;The gateway operates as a single front door. When a request arrives — say, a mobile app asking for a customer's order history — the gateway performs several steps before any backend service ever sees the request.&lt;/p&gt;

&lt;p&gt;First, it verifies identity. Is the caller who they claim to be? This is authentication. The gateway checks a credential (a token, an API key) so that no individual service needs to implement its own login logic.&lt;/p&gt;

&lt;p&gt;Second, it checks permissions. Is this caller allowed to access this resource? A partner integration might access order data but not payment details. The gateway enforces this boundary.&lt;/p&gt;

&lt;p&gt;Third, it routes the request to the right service. The caller addresses the gateway at one address; the gateway knows where each internal service lives and forwards accordingly.&lt;/p&gt;

&lt;p&gt;Fourth, it records everything. Who called, what they asked for, how long it took, whether it succeeded. This audit trail feeds dashboards, billing systems, and security alerts.&lt;/p&gt;

&lt;p&gt;The response follows the same path in reverse. The backend service answers the gateway; the gateway returns the answer to the caller. From the caller's perspective, there is one system. Behind the gateway, there can be dozens.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cloud Provider Options
&lt;/h2&gt;

&lt;p&gt;All three major cloud providers offer managed API gateway services. Choosing between them is primarily a question of where your existing infrastructure lives — not a question of technical superiority.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Product&lt;/th&gt;
&lt;th&gt;Best Fit&lt;/th&gt;
&lt;th&gt;Pricing Model&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AWS&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Amazon API Gateway&lt;/td&gt;
&lt;td&gt;Organizations already on AWS (Lambda, EC2, ECS)&lt;/td&gt;
&lt;td&gt;Per million API calls — HTTP APIs from $1.00/million, REST APIs from $3.50/million&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Azure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Azure API Management (APIM)&lt;/td&gt;
&lt;td&gt;Organizations on Microsoft Azure or using Microsoft 365, Active Directory&lt;/td&gt;
&lt;td&gt;Tiered monthly plans — Developer tier from ~$50/month, Production tiers from ~$285/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Google Cloud&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GCP API Gateway / Apigee&lt;/td&gt;
&lt;td&gt;Organizations on Google Cloud or requiring advanced analytics and monetization&lt;/td&gt;
&lt;td&gt;Per call pricing for API Gateway; Apigee uses capacity-based enterprise contracts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;How to choose:&lt;/strong&gt; start with the cloud you already use. Switching cloud providers to get a different API gateway is never worth it — the gateway features across all three are comparable for standard use cases. If you are not yet committed to a cloud, I recommend AWS — the ecosystem integration, documentation depth, and available talent pool are hard to beat for most organizations. If you require enterprise-grade analytics, developer portals, and API monetization, Apigee (Google's enterprise offering) leads the market — Gartner named it a leader in the 2025 Magic Quadrant for API Management, and in practice it is the only realistic choice when API monetization is a business requirement rather than a nice-to-have.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pros and Cons
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Advantages
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Single security enforcement point:&lt;/strong&gt; Patch a vulnerability or change an authentication policy in one place rather than across every service simultaneously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operational leverage:&lt;/strong&gt; One engineering team can manage gateway policies that protect dozens of backend services — a force multiplier on your security and platform teams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Faster product iteration:&lt;/strong&gt; Backend teams change their services without coordinating with every client team. The gateway contract stays stable while implementations evolve.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measurable traffic data:&lt;/strong&gt; First time many organizations have a clear view of who uses their APIs, at what volume, and with what error rates — information that directly informs pricing, capacity planning, and product decisions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Disadvantages and Risks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Added infrastructure cost:&lt;/strong&gt; A managed gateway adds a recurring cloud bill. For low-traffic applications, this cost may exceed the operational savings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Single point of failure if misconfigured:&lt;/strong&gt; Centralizing all traffic through one layer means a gateway outage affects all services simultaneously. I've seen this play out when a team deployed a self-hosted Kong instance without HA configuration — a single bad config push took down every client-facing service for forty minutes during business hours, something that would have been impossible in the pre-gateway architecture where each service failed independently. Mitigation: use managed cloud offerings with built-in redundancy, not self-hosted solutions, unless you have a dedicated platform team.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Latency addition:&lt;/strong&gt; Every request passes through one more network hop. The overhead is typically 1–5 milliseconds — negligible for most applications, but worth knowing if you are building latency-sensitive real-time systems.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vendor dependency:&lt;/strong&gt; Choosing a cloud-native gateway creates some lock-in to that provider's configuration model. Open-source alternatives (Kong, Traefik) reduce lock-in at the cost of more operational complexity. For most organizations below 200 engineers, I'd take the managed vendor lock-in — the operational burden of running your own gateway infrastructure is not a trade worth making until you have a dedicated platform team to absorb it.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Is an API Gateway Right for You?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvbmfhypyqo9mwhl2a00w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvbmfhypyqo9mwhl2a00w.png" alt="Is an API Gateway Right for you?" width="800" height="723"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Adopt if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You run three or more backend services&lt;/li&gt;
&lt;li&gt;You serve multiple client types (mobile, web, partner APIs, internal tools)&lt;/li&gt;
&lt;li&gt;Security, compliance, or audit logging is a business requirement&lt;/li&gt;
&lt;li&gt;Your engineering team spends time duplicating authentication or monitoring logic across services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Wait if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your team has fewer than five engineers and you are still validating product-market fit&lt;/li&gt;
&lt;li&gt;You have one client and one backend — the overhead is not justified&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Avoid if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have a single, simple application with one consumer and no plans to expand&lt;/li&gt;
&lt;li&gt;Your API is internal-only with no external partners or clients&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Adoption Roadmap
&lt;/h2&gt;

&lt;p&gt;A realistic four-phase approach for an organization starting from zero.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv8z9j749sgqeif1iv9wf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv8z9j749sgqeif1iv9wf.png" alt="Adoption Roadmap" width="800" height="127"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Evaluate (Weeks 1–2)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Audit how many backend services you currently run and how many distinct clients access them.&lt;/li&gt;
&lt;li&gt;Identify the single most painful integration point — the one your engineering team mentions most often in planning meetings.&lt;/li&gt;
&lt;li&gt;Assign one technical owner and one business sponsor.&lt;/li&gt;
&lt;li&gt;Define success criteria before starting: reduced incident response time, reduced time to onboard a new partner, or cost reduction in a specific area.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Phase 2: Pilot (Weeks 3–6)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Implement the gateway for one non-critical service — not your core payment or authentication system. In practice, a reporting API or a read-only catalog endpoint is the right first target: low blast radius if something goes wrong, real enough traffic to surface configuration issues.&lt;/li&gt;
&lt;li&gt;Route one client type through the gateway only.&lt;/li&gt;
&lt;li&gt;Measure against your success criteria weekly. Track gateway latency overhead, incidents, and team time spent on configuration.&lt;/li&gt;
&lt;li&gt;Decision point at end of week six: continue, adjust, or stop.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Phase 3: Expand (Months 2–4)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Apply lessons from the pilot to your highest-priority services.&lt;/li&gt;
&lt;li&gt;Build an internal runbook: how to add a new service, how to add a new client, how to change an access policy.&lt;/li&gt;
&lt;li&gt;Train additional team members on gateway administration.&lt;/li&gt;
&lt;li&gt;Review actual costs against initial estimates — cloud bills often surprise at this stage.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Phase 4: Standardize (Month 4 onward)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Establish organizational standards: every new service goes through the gateway from day one.&lt;/li&gt;
&lt;li&gt;Set up automated cost alerts at 80% and 100% of monthly budget.&lt;/li&gt;
&lt;li&gt;Integrate gateway access logs into your security incident response process.&lt;/li&gt;
&lt;li&gt;Review and rotate API credentials on a defined schedule.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Cost Considerations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost Type&lt;/th&gt;
&lt;th&gt;What to Budget For&lt;/th&gt;
&lt;th&gt;Typical Range&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API call volume&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Primary usage charge — per million requests processed&lt;/td&gt;
&lt;td&gt;$1–$3.50 per million (AWS); varies by provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Data transfer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Outbound data charged separately by cloud provider&lt;/td&gt;
&lt;td&gt;$0.09/GB (AWS); similar on Azure and GCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Caching&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Optional: reduces backend load, adds a fixed hourly cost&lt;/td&gt;
&lt;td&gt;$20–$80/month for a dedicated cache layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Managed tier subscription&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Azure APIM and enterprise tiers charge monthly flat fees&lt;/td&gt;
&lt;td&gt;$50–$2,000+/month depending on tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Engineering setup time&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;One-time implementation cost&lt;/td&gt;
&lt;td&gt;2–6 weeks of one engineer's time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ongoing operations&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Configuration updates, policy changes, incident response&lt;/td&gt;
&lt;td&gt;10–20% of initial setup effort per year&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;ROI signal:&lt;/strong&gt; if your engineering team spends more than two days per month resolving authentication or integration issues across multiple services, the operational savings from a centralized gateway typically offset its cost within the first six months. The teams I've worked with consistently underestimate this — they track the gateway's monthly cloud bill but not the engineering hours it replaces. Run the actual calculation before your budget discussion: hourly engineering cost times recurring integration hours is almost always the bigger number.&lt;/p&gt;

&lt;p&gt;For a startup or small team processing under 10 million API calls per month, expect $50–$200/month on a managed cloud gateway. Mid-size organizations at 100–500 million monthly calls should budget $500–$3,000/month, depending on caching and data transfer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Questions to Ask Your Engineering Team
&lt;/h2&gt;

&lt;p&gt;Before committing, get clear answers to these:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;"How many backend services do we currently run, and how many will we add in the next twelve months?"&lt;/li&gt;
&lt;li&gt;"Where are we today duplicating authentication, logging, or rate-limiting logic across services?"&lt;/li&gt;
&lt;li&gt;"What is our current security incident response time when a vulnerability affects an API?"&lt;/li&gt;
&lt;li&gt;"Which cloud provider do we already use, and does their gateway offering cover our requirements?"&lt;/li&gt;
&lt;li&gt;"Who will own gateway configuration — is this a full-time concern or a part-time responsibility?"&lt;/li&gt;
&lt;li&gt;"What is our monthly API call volume today, and what does it look like in two years at our projected growth?"&lt;/li&gt;
&lt;li&gt;"What does the failure mode look like if the gateway goes down? Do we have a fallback plan?"&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;An API gateway is not a technology bet — it is an operational decision about how much complexity you are willing to distribute across your engineering organization. Every organization past the startup stage is running this complexity somewhere; the question is whether it is centralized and manageable or scattered across ten services and invisible. The organizations that centralize it earlier move faster, respond to incidents faster, and spend less time on the plumbing that enables their actual product. If you take one thing from this article: the cost of the gateway is visible on your cloud bill; the cost of not having one is buried in engineering hours, incident postmortems, and integration timelines that nobody is tracking. Evaluate it now, before the complexity makes the decision for you.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/api-gateway/" rel="noopener noreferrer"&gt;Amazon API Gateway — Product Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://azure.microsoft.com/en-us/products/api-management" rel="noopener noreferrer"&gt;Azure API Management — Product Overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/api-gateway/docs" rel="noopener noreferrer"&gt;Google Cloud API Gateway Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.gartner.com/en/documents/7049798" rel="noopener noreferrer"&gt;Gartner Market Guide for API Gateways&lt;/a&gt; &lt;em&gt;(requires Gartner subscription)&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.ibm.com/think/topics/api-gateway" rel="noopener noreferrer"&gt;IBM: What Is an API Gateway?&lt;/a&gt; — vendor-neutral conceptual overview&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bry Writes Code — cloud and AI infrastructure specialist. Evaluating whether an API gateway is the right fit for your organization? &lt;a href="mailto:brywritescode@gmail.com"&gt;Let's talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>api</category>
      <category>webdev</category>
      <category>devops</category>
    </item>
    <item>
      <title>Azure API Management: When Policies Beat a Hand-Rolled Gateway</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Tue, 28 Jul 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/azure-api-management-when-policies-beat-a-hand-rolled-gateway-2pom</link>
      <guid>https://dev.to/brywritescode/azure-api-management-when-policies-beat-a-hand-rolled-gateway-2pom</guid>
      <description>&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Azure API Management (APIM) is worth the setup cost the moment you'd otherwise write custom gateway code for auth, rate limiting, or protocol translation — its policy engine replaces that code with declarative XML.&lt;/li&gt;
&lt;li&gt;The Azure CLI's &lt;code&gt;az apim&lt;/code&gt; command group covers instances, APIs, operations, products, and version sets — but has no command for setting a policy document, a real gap in the CLI module. The workaround is &lt;code&gt;az rest&lt;/code&gt; against the ARM REST API directly, exactly what Azure's own CLI maintainers point to when asked.&lt;/li&gt;
&lt;li&gt;Non-Consumption APIM tiers take up to 40 minutes to provision. Plan your CI pipeline and your patience around that, not around how fast &lt;code&gt;az apim create&lt;/code&gt; returns.&lt;/li&gt;
&lt;li&gt;Consumption tier looks free-tier attractive but has no SLA on the developer portal — treat Standard or Standard v2 as the real production floor.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CLI/SDK version tested against: &lt;code&gt;az-cli 2.6x.x&lt;/code&gt; (Azure CLI's &lt;code&gt;apim&lt;/code&gt; extension ships in-box on current 2.6x releases — run &lt;code&gt;az version&lt;/code&gt; to confirm)&lt;/li&gt;
&lt;li&gt;An Azure subscription with &lt;code&gt;Microsoft.ApiManagement&lt;/code&gt; resource provider registered (&lt;code&gt;az provider register --namespace Microsoft.ApiManagement&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A backend HTTP service to import — the examples below use a placeholder &lt;code&gt;https://backend.example.com&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Most gateway comparisons put Azure APIM next to AWS API Gateway and Kong and stop at a feature checklist. That's the wrong first question. The right one is: are you about to write custom code to do something a policy engine already does declaratively? If the answer is yes — rate limiting per subscription key, JWT validation before the request reaches your backend, REST-to-SOAP translation for a legacy service — APIM's policy engine is doing real work for you, not just adding a hop.&lt;/p&gt;

&lt;p&gt;I've stood up APIM for an enterprise client that needed exactly this: fifty internal APIs, one gateway, centrally enforced auth and rate limiting without touching a single backend service's code. The CLI got most of the way there cleanly. Then I hit the one thing nobody warns you about: there's no &lt;code&gt;az apim policy set&lt;/code&gt; command. It doesn't exist. You'll go looking for it, because every other resource in APIM has an obvious CLI verb, and this one just... doesn't.&lt;/p&gt;

&lt;p&gt;This article builds a real APIM instance end to end from the terminal — instance, API import, and a working policy document — and shows you the actual workaround for the missing piece.&lt;/p&gt;




&lt;h2&gt;
  
  
  What APIM Actually Is
&lt;/h2&gt;

&lt;p&gt;APIM is not a thin proxy. It's three components that happen to share one Azure resource:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frhwu6y90ysn0k0betf47.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frhwu6y90ysn0k0betf47.png" alt="What APIM Actually Is" width="756" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: the gateway, the management plane, and the developer portal are three separable concerns inside one APIM instance.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The policy engine is the part that actually replaces custom code. Policies are XML documents that run in a defined order — inbound, backend, outbound, on-error — and attach at four scopes: global, product, API, or a single operation. A five-line policy can do what would otherwise be a Lambda-equivalent function on AWS or a custom middleware layer anywhere else.&lt;/p&gt;




&lt;h2&gt;
  
  
  Standing Up the Instance
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az group create &lt;span class="nt"&gt;--name&lt;/span&gt; rg-apim-demo &lt;span class="nt"&gt;--location&lt;/span&gt; eastus

&lt;span class="c"&gt;# Non-Consumption tiers take up to 40 minutes to provision.&lt;/span&gt;
&lt;span class="c"&gt;# Consumption tier (sku-name Consumption) provisions in under 5.&lt;/span&gt;
az apim create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; apim-demo-001 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; rg-apim-demo &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--publisher-email&lt;/span&gt; platform-team@example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--publisher-name&lt;/span&gt; &lt;span class="s2"&gt;"Example Platform Team"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--sku-name&lt;/span&gt; Developer &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--no-wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--no-wait&lt;/code&gt; matters here. If you're scripting this in CI, don't block the pipeline on a 40-minute provision — poll with &lt;code&gt;az apim wait --created&lt;/code&gt; on a separate step, or trigger the API import as a follow-up job once provisioning completes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az apim &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="nt"&gt;--created&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; apim-demo-001 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; rg-apim-demo &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timeout&lt;/span&gt; 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Importing an API
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az apim api import &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; rg-apim-demo &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service-name&lt;/span&gt; apim-demo-001 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--api-id&lt;/span&gt; orders-api &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--path&lt;/span&gt; orders &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--display-name&lt;/span&gt; &lt;span class="s2"&gt;"Orders API"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service-url&lt;/span&gt; https://backend.example.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--specification-format&lt;/span&gt; OpenApi &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--specification-url&lt;/span&gt; https://backend.example.com/openapi.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subscription-required&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gets you a routable API in front of your backend, with subscription-key enforcement on by default. What it doesn't get you is anything from the policy engine — no rate limiting, no JWT validation, no header rewriting. For that, keep reading.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Policy Gap — and the Workaround
&lt;/h2&gt;

&lt;p&gt;Every other APIM resource has a matching &lt;code&gt;az apim&lt;/code&gt; verb: &lt;code&gt;az apim api create&lt;/code&gt;, &lt;code&gt;az apim product create&lt;/code&gt;, &lt;code&gt;az apim api operation create&lt;/code&gt;. Look for the policy equivalent and you won't find one — &lt;code&gt;az apim api&lt;/code&gt;'s subcommand list covers create/update/delete/export/import/operation/release/revision/schema/versionset, and stops there. No &lt;code&gt;policy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk8g4ids4o4hlgjrbjbhp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk8g4ids4o4hlgjrbjbhp.png" alt="The Policy Gap — and the Workaround" width="645" height="902"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: decision path once you hit the policy gap in the CLI.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The documented-nowhere-obvious answer, confirmed by Azure CLI maintainers on the &lt;code&gt;azure-cli&lt;/code&gt; GitHub repo when the same question was raised: call the ARM REST API directly with &lt;code&gt;az rest&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Write the policy document to a file first.&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; policy.xml &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
&amp;lt;policies&amp;gt;
  &amp;lt;inbound&amp;gt;
    &amp;lt;base /&amp;gt;
    &amp;lt;rate-limit-by-key calls="100" renewal-period="60"
                        counter-key="@(context.Subscription.Id)" /&amp;gt;
    &amp;lt;set-header name="X-Forwarded-By" exists-action="override"&amp;gt;
      &amp;lt;value&amp;gt;apim-demo-001&amp;lt;/value&amp;gt;
    &amp;lt;/set-header&amp;gt;
  &amp;lt;/inbound&amp;gt;
  &amp;lt;backend&amp;gt;&amp;lt;base /&amp;gt;&amp;lt;/backend&amp;gt;
  &amp;lt;outbound&amp;gt;&amp;lt;base /&amp;gt;&amp;lt;/outbound&amp;gt;
  &amp;lt;on-error&amp;gt;&amp;lt;base /&amp;gt;&amp;lt;/on-error&amp;gt;
&amp;lt;/policies&amp;gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;&lt;span class="c"&gt;# Wrap the XML in the JSON body the ARM policy resource expects.&lt;/span&gt;
&lt;span class="nv"&gt;POLICY_XML&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat &lt;/span&gt;policy.xml | python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'import json,sys; print(json.dumps(sys.stdin.read()))'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

az rest &lt;span class="nt"&gt;--method&lt;/span&gt; put &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--uri&lt;/span&gt; &lt;span class="s2"&gt;"https://management.azure.com/subscriptions/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SUBSCRIPTION_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/resourceGroups/rg-apim-demo/providers/Microsoft.ApiManagement/service/apim-demo-001/apis/orders-api/policies/policy?api-version=2022-08-01"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;properties&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;format&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;xml&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;value&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;POLICY_XML&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;}}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single &lt;code&gt;az rest&lt;/code&gt; call does what a &lt;code&gt;Set-AzApiManagementPolicy&lt;/code&gt; PowerShell cmdlet or a portal paste into the policy editor would otherwise do — and it's the only way to keep a policy deployment in a bash-based CI pipeline without shelling out to PowerShell Core as a second runtime dependency.&lt;/p&gt;

&lt;p&gt;Verify it landed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az rest &lt;span class="nt"&gt;--method&lt;/span&gt; get &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--uri&lt;/span&gt; &lt;span class="s2"&gt;"https://management.azure.com/subscriptions/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SUBSCRIPTION_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/resourceGroups/rg-apim-demo/providers/Microsoft.ApiManagement/service/apim-demo-001/apis/orders-api/policies/policy?api-version=2022-08-01"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'properties.value'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; tsv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Tier Comparison
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Approx. price&lt;/th&gt;
&lt;th&gt;SLA&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Consumption&lt;/td&gt;
&lt;td&gt;Pay-per-call, 1M free/month&lt;/td&gt;
&lt;td&gt;No SLA on developer portal&lt;/td&gt;
&lt;td&gt;Fastest to provision (minutes), but skip it for anything customer-facing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Developer&lt;/td&gt;
&lt;td&gt;~$50/month&lt;/td&gt;
&lt;td&gt;None — dev/test only&lt;/td&gt;
&lt;td&gt;Use for exactly what the name says&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;td&gt;~$150/month&lt;/td&gt;
&lt;td&gt;Yes, limited scale&lt;/td&gt;
&lt;td&gt;Entry production tier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;td&gt;~$700/month&lt;/td&gt;
&lt;td&gt;Yes, full policy engine&lt;/td&gt;
&lt;td&gt;Practical production floor for most teams&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Standard v2&lt;/td&gt;
&lt;td&gt;Comparable to Standard&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Adds network-isolated backend support without Premium's full VNET cost&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Premium&lt;/td&gt;
&lt;td&gt;~$2,700+/month per unit&lt;/td&gt;
&lt;td&gt;99.95%, multi-region&lt;/td&gt;
&lt;td&gt;Multi-region active-active, VNET injection, availability zones&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I default clients to Standard or Standard v2 for production. Consumption's lack of a developer-portal SLA and reduced policy feature set makes it a poor fit the moment external partners depend on your API catalog being reliably browsable — which is the entire point of having a developer portal in the first place.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1: Assuming &lt;code&gt;az apim&lt;/code&gt; is a complete CLI surface&lt;/strong&gt;&lt;br&gt;
It covers structural resources well. It does not cover policies. Budget time to discover and use &lt;code&gt;az rest&lt;/code&gt; before you're blocked on a deploy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2: Blocking a CI pipeline on &lt;code&gt;az apim create&lt;/code&gt; without &lt;code&gt;--no-wait&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
Non-Consumption tiers take up to 40 minutes. A pipeline step with a default timeout will fail long before provisioning completes if you don't split create and wait into separate steps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3: Choosing Consumption tier for a customer-facing API catalog&lt;/strong&gt;&lt;br&gt;
The missing developer-portal SLA isn't a rounding-error limitation — if your onboarding flow depends on partners self-serving through the portal, an unreliable portal is a support-ticket generator.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 4: Writing policy XML by hand in the portal and never committing it to source control&lt;/strong&gt;&lt;br&gt;
The &lt;code&gt;az rest&lt;/code&gt; path above works precisely because it turns policy deployment into something you can put in a repo and a pipeline. Portal-only policy edits are invisible to code review and impossible to diff.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production Considerations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Performance:&lt;/strong&gt; Policy chain complexity adds latency — a chain with JWT validation, rate limiting, and header manipulation adds measurably more than a single &lt;code&gt;rate-limit-by-key&lt;/code&gt; policy alone. Profile with Application Insights before assuming the policy engine is "free."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security:&lt;/strong&gt; Pair APIM with Azure AD (Entra ID) for OAuth flows rather than rolling your own JWT validation policy from scratch — &lt;code&gt;validate-jwt&lt;/code&gt; with an &lt;code&gt;openid-config&lt;/code&gt; reference is less code to maintain than a custom Lambda-equivalent would be on another cloud.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; The gap between Consumption and Standard is real money, but the gap between under-provisioning Standard and needing Premium's VNET injection for compliance is bigger. Confirm your network isolation requirements before committing to a tier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitoring:&lt;/strong&gt; Enable Application Insights integration at creation time, not after. Track Backend Duration separately from Gateway Duration — this is how you tell whether a slow response is APIM's policy chain or the backend service itself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full Example: Policy Deployment Script
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

&lt;span class="nv"&gt;RG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RG&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;rg&lt;/span&gt;&lt;span class="p"&gt;-apim-demo&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;SERVICE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SERVICE&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;apim&lt;/span&gt;&lt;span class="p"&gt;-demo-001&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;API_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;API_ID&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;-api&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;SUBSCRIPTION_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az account show &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;

deploy_policy&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;policy_file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;policy_xml
  &lt;span class="nv"&gt;policy_xml&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import json,sys; print(json.dumps(open('&lt;/span&gt;&lt;span class="nv"&gt;$policy_file&lt;/span&gt;&lt;span class="s2"&gt;').read()))"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

  az rest &lt;span class="nt"&gt;--method&lt;/span&gt; put &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--uri&lt;/span&gt; &lt;span class="s2"&gt;"https://management.azure.com/subscriptions/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SUBSCRIPTION_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/resourceGroups/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;RG&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/providers/Microsoft.ApiManagement/service/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SERVICE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/apis/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;API_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/policies/policy?api-version=2022-08-01"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;properties&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;format&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;xml&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;value&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;policy_xml&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;}}"&lt;/span&gt;

  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Deployed policy from &lt;/span&gt;&lt;span class="nv"&gt;$policy_file&lt;/span&gt;&lt;span class="s2"&gt; to &lt;/span&gt;&lt;span class="nv"&gt;$API_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

deploy_policy &lt;span class="s2"&gt;"policy.xml"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Full source with a matching teardown script: &lt;a href="https://github.com/brywritescode/bry-writes-code-examples.git" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; → &lt;code&gt;cloud-apis/azure-api-management-policies/&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;APIM earns its keep the moment its policy engine replaces code you'd otherwise write and maintain yourself — auth validation, rate limiting, protocol translation. Standard or Standard v2 is the right production tier for nearly everyone; Consumption's missing portal SLA makes it a false economy for anything customer-facing. And when the CLI's &lt;code&gt;az apim&lt;/code&gt; command group leaves you stuck on policies specifically, that's not a sign you did something wrong — it's a real, if quietly undocumented, gap that &lt;code&gt;az rest&lt;/code&gt; closes in one call.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/cli/azure/apim?view=azure-cli-latest" rel="noopener noreferrer"&gt;az apim — Microsoft Learn CLI reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/api-management/api-management-features" rel="noopener noreferrer"&gt;Feature-based comparison of Azure API Management tiers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/api-management/v2-service-tiers-overview" rel="noopener noreferrer"&gt;Azure API Management — V2 Tiers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://azure.microsoft.com/en-us/pricing/details/api-management/" rel="noopener noreferrer"&gt;API Management pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/api-management/get-started-create-service-instance-cli" rel="noopener noreferrer"&gt;Quickstart: Create an APIM instance using the Azure CLI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bry Writes Code — cloud and API infrastructure specialist. Standing up an enterprise API program on Azure? &lt;a href="mailto:brywritescode@gmail.com"&gt;Get in touch&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>azure</category>
      <category>typescript</category>
      <category>backend</category>
    </item>
    <item>
      <title>Japan Market Entry for Cloud &amp; SaaS Vendors: What Localization Actually Requires Beyond Translation</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Thu, 23 Jul 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/japan-market-entry-for-cloud-saas-vendors-what-localization-actually-requires-beyond-translation-2ji6</link>
      <guid>https://dev.to/brywritescode/japan-market-entry-for-cloud-saas-vendors-what-localization-actually-requires-beyond-translation-2ji6</guid>
      <description>&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Translation is table stakes, not the differentiator — Japanese enterprise buyers evaluate data residency, ISMAP-equivalent security posture, and support-hour coverage before they evaluate your feature list.&lt;/li&gt;
&lt;li&gt;Japan's information density norms invert Western UX defaults: a dashboard that reads as "clean" to a US buyer reads as sparse and untrustworthy to a Japanese one.&lt;/li&gt;
&lt;li&gt;Data residency has moved from a nice-to-have to a procurement gate in the last few enterprise cycles — "we can serve requests from a US region" is now a disqualifying answer, not a caveat.&lt;/li&gt;
&lt;li&gt;The RFP-to-contract cycle runs longer than most Western go-to-market plans budget for, and rushing it reads as a lack of commitment, not efficiency.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I've watched foreign SaaS vendors run the same playbook entering Japan: hire a translation agency, localize the marketing site, hire a country manager, and expect the pipeline to behave like it did in the UK or Germany. Six months later the pipeline is full of "interested, evaluating" deals that never close, and nobody on the team can explain why the product that won in three other markets is stalling in this one.&lt;/p&gt;

&lt;p&gt;The answer is rarely the product. It's that Japan's enterprise buying process evaluates a different set of signals than most Western markets do, and localization done as a translation exercise never touches any of them. This is the first article I am writing about this topic drawing on my own experience as a Tokyo-based engineer navigating both sides of this: implementing systems Japanese enterprises buy, and helping foreign teams understand why their otherwise-solid pitch isn't landing.&lt;/p&gt;

&lt;p&gt;This covers what "localization" actually needs to include for a cloud or SaaS product to clear a Japanese enterprise buying committee, not just a Japanese user's browser.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Localization Actually Means Here
&lt;/h2&gt;

&lt;p&gt;Most vendors treat localization as a translation project: strings into Japanese, maybe a JP-language support inbox, done. Think of it instead like localizing a car for a market that drives on the other side of the road — you don't just relabel the pedals, you move the steering wheel, and if you skip that step the car is unsellable regardless of how good the engine is.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem it solves:&lt;/strong&gt; a translated-only product fails at the point where a Japanese buyer's procurement, legal, and security teams start asking questions your localization checklist never anticipated — where is the data stored, what's your incident notification SLA under Japanese law, does your interface actually match how a Japanese team works, not just what language it displays in.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmwpxz4ple10teq45w3jt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmwpxz4ple10teq45w3jt.png" alt="What Localization Actually Means Here" width="784" height="308"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three categories do the actual work, and translation is only one of them:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Product and UX localization&lt;/strong&gt; — beyond string translation, into information density, workflow assumptions, and support-channel expectations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regulatory and security posture&lt;/strong&gt; — APPI compliance, data residency, and (for anything touching government or large-enterprise procurement) ISMAP alignment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commercial and contractual localization&lt;/strong&gt; — yen-denominated pricing, dual-language contracts under Japanese governing law, and a sales process built for a longer, more consensus-driven buying cycle.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Product Localization: Deeper Than Strings
&lt;/h2&gt;

&lt;p&gt;Japanese enterprise software users expect information-dense interfaces. Where a US-market dashboard leans toward whitespace and a handful of headline metrics, the Japanese-market equivalent that wins in comparison testing tends to expose more fields, more status detail, and more explicit labeling — the same design instinct that produces Japan's famously dense transit maps and product packaging. A UI a US design team calls "clean" often reads to a Japanese buyer as incomplete, or worse, as a product that's hiding something.&lt;/p&gt;

&lt;p&gt;Documentation carries more procurement weight here too. Japanese enterprise buyers lean on manuals, in-product help, and tooltips more heavily than a comparable Western buyer evaluating the same product, and a help center that's obviously auto-translated (mixed terminology, awkward phrasing, missing sections) reads as a signal the vendor isn't serious about the market — not a minor rough edge. Any interface where English strings leak through mid-flow (an error message that didn't get localized, a settings page half in English) gets flagged the same way a broken link gets flagged in a security review: evidence the product wasn't built with this market in mind.&lt;/p&gt;

&lt;p&gt;Support-hour coverage matters concretely, not symbolically: JST business-hours support, staffed by people who can actually read the ticket without a translation round-trip, is close to a hard requirement for anything positioned above small-business self-serve.&lt;/p&gt;




&lt;h2&gt;
  
  
  Regulatory Posture: APPI, Data Residency, and ISMAP
&lt;/h2&gt;

&lt;p&gt;This section is a map of what to expect, not a substitute for Japanese counsel — treat the specifics below as the shape of the requirements, and get a licensed opinion before you commit language to a contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;APPI (Act on the Protection of Personal Information)&lt;/strong&gt; is Japan's core data protection law, enforced by the Personal Information Protection Commission (PPC). A qualifying breach requires an initial report to the PPC within a matter of days and a detailed follow-up report within roughly two months — timelines tight enough that "we'll figure out our breach process if it happens" isn't a credible answer in a security questionnaire. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data residency&lt;/strong&gt; has hardened from a differentiator into a procurement gate over the last several enterprise cycles. "Our infrastructure is multi-region and includes APAC" used to be an acceptable hedge. For enterprise-tier deals today, "can you guarantee this customer's data never leaves Japan" is frequently a yes/no gate a "we're flexible" answer fails outright — not a point to negotiate after the technical evaluation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ISMAP (Information system Security Management and Assessment Program)&lt;/strong&gt; is the Japanese government's cloud security assessment framework, run jointly by the Cabinet Secretariat's National center of Incident readiness and Strategy for Cybersecurity and the Information-technology Promotion Agency. You don't need ISMAP registration to sell to private enterprise — it's formally a government-procurement requirement — but its control baseline has become a reference point large private-sector security teams cite in vendor questionnaires anyway, the same way SOC 2 became a de facto private-sector bar in the US despite starting as an audit standard for service organizations. ISMAP-LIU, a lighter-weight track introduced for lower-risk SaaS, is worth knowing about specifically because it's a materially smaller lift than the full framework and fits a large share of horizontal SaaS products.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F18vx8jgljpaahlv0olzz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F18vx8jgljpaahlv0olzz.png" alt="Regulatory Posture: APPI, Data Residency, and ISMAP" width="784" height="674"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Commercial Localization: Contracts, Pricing, and the Buying Cycle
&lt;/h2&gt;

&lt;p&gt;Yen-denominated pricing isn't optional past the pilot stage — a USD invoice reads as "this vendor hasn't actually committed to this market" to a procurement team weighing that signal alongside your competitors' localized contracts.&lt;/p&gt;

&lt;p&gt;Contracts should exist in Japanese and English side by side, not Japanese-only or English-only, with Japanese governing law stated explicitly. Dual-language isn't redundancy: it gives the Japanese legal reviewer a document they can actually mark up and gives your own counsel a version they can actually defend, and misalignment between the two versions is a real risk worth a proper legal translation pass, not a machine-translation shortcut.&lt;/p&gt;

&lt;p&gt;Enterprise purchase approval in Japan typically routes through several sequential sign-offs — section chief, department head, executive director — each a real decision point, not a formality to route around. That's structurally why the cycle runs longer than the Western sales motion most foreign teams plan their quarter around, and why a vendor pushing for a faster close reads as impatient rather than efficient. The takeaway is that your localization timeline and your sales-cycle timeline need to be planned together, not sequentially — starting the sales conversation before the product, docs, and contract templates are actually ready burns the first-impression credibility you don't get to spend twice with the same buyer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Comparison: Translation-Only vs Localized Launch vs Full Market-Entry Program
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criteria&lt;/th&gt;
&lt;th&gt;Translation-Only&lt;/th&gt;
&lt;th&gt;Localized Launch&lt;/th&gt;
&lt;th&gt;Full Market-Entry Program&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What's included&lt;/td&gt;
&lt;td&gt;UI strings + marketing site in Japanese&lt;/td&gt;
&lt;td&gt;+ UX redesign, JST support, dual-language contracts, yen pricing&lt;/td&gt;
&lt;td&gt;+ local entity, ISMAP/ISMAP-LIU pursuit, dedicated JP sales team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time to first enterprise contract&lt;/td&gt;
&lt;td&gt;Rarely closes above SMB tier&lt;/td&gt;
&lt;td&gt;6–12 months&lt;/td&gt;
&lt;td&gt;12–24 months&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Low (translation agency fees)&lt;/td&gt;
&lt;td&gt;Moderate (design + support + legal)&lt;/td&gt;
&lt;td&gt;High (entity setup, compliance program, headcount)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Enterprise procurement pass rate&lt;/td&gt;
&lt;td&gt;Low — stalls at security/data-residency questions&lt;/td&gt;
&lt;td&gt;Moderate — clears mid-market, struggles at large-enterprise/government tier&lt;/td&gt;
&lt;td&gt;High — built to clear the tier it's targeting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Testing product-market signal cheaply before committing&lt;/td&gt;
&lt;td&gt;Mid-market and SMB-enterprise SaaS&lt;/td&gt;
&lt;td&gt;Enterprise and government-adjacent sales motions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Bottom line:&lt;/strong&gt; treat translation-only as a signal-gathering step, not a market-entry strategy — use it to validate interest, then budget for the localized-launch tier before your first real enterprise sales cycle, and reserve the full program for when a specific large-enterprise or public-sector deal justifies the entity and compliance investment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pros and Cons
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Advantages of Entering Properly
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Durable relationships:&lt;/strong&gt; Japanese enterprise buyers who complete a first contract with a vendor that respected the process tend to expand slowly but reliably — churn among properly-onboarded enterprise accounts is low relative to Western SaaS norms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Referenceable wins compound:&lt;/strong&gt; a single credible enterprise logo in Japan opens doors with peer companies in the same keiretsu or industry association faster than the equivalent reference does in a more fragmented Western market.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Less competitive crowding at the top tier:&lt;/strong&gt; many foreign vendors give up at the translation-only stage, so the localized and full-program tiers have real, addressable whitespace.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Disadvantages and Risks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sunk cost before revenue:&lt;/strong&gt; a full market-entry program is a real capital commitment before the first enterprise contract closes — don't start it speculatively.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;APPI and data-residency misses are expensive to unwind:&lt;/strong&gt; an architecture built assuming cross-region flexibility is a real re-platforming project to fix later, not a config change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A rushed first impression is hard to recover from:&lt;/strong&gt; unlike markets where a bad first pitch just costs you that one deal, a vendor seen as underprepared in Japan's tighter B2B community can carry that reputation into the next three conversations.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Is This Right for You?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2zsak55zjdps3e5frd2f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2zsak55zjdps3e5frd2f.png" alt="Is This Right for You?" width="784" height="1214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enter now if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have inbound interest or a named target account already asking about Japan availability.&lt;/li&gt;
&lt;li&gt;Your architecture can commit to Japan-only data residency without a rebuild.&lt;/li&gt;
&lt;li&gt;Leadership is prepared to plan around a longer sales cycle, not shorten it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Wait if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have no specific signal yet — test with a localized-launch tier aimed at mid-market before committing to full program cost.&lt;/li&gt;
&lt;li&gt;Your product's data architecture assumes cross-region flexibility you're not ready to change.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Skip for now if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your total addressable market in Japan is speculative and no other APAC or domestic priority is a stronger use of the same budget this year.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Implementation Approach
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Phase 1: Validate Signal (Months 1–2)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Localize marketing materials and a demo environment into Japanese — translation tier, deliberately minimal spend.&lt;/li&gt;
&lt;li&gt;Identify 3–5 named target accounts or work existing inbound interest.&lt;/li&gt;
&lt;li&gt;Run discovery calls with a bilingual team member present, not a translation tool.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Phase 2: Localized Launch (Months 3–8)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Commission a real UX localization pass — information density, workflow assumptions, not just string translation.&lt;/li&gt;
&lt;li&gt;Stand up JST-hours support with staff who read tickets natively.&lt;/li&gt;
&lt;li&gt;Draft dual-language contract templates with Japanese governing law, reviewed by Japan-qualified counsel.&lt;/li&gt;
&lt;li&gt;Set yen-denominated pricing.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Phase 3: Compliance and Entity (Months 9–18, if pursuing enterprise/government tier)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Confirm data residency architecture — Japan-region hosting for Japan customer data, verified, not assumed.&lt;/li&gt;
&lt;li&gt;Begin APPI compliance program formally.&lt;/li&gt;
&lt;li&gt;Evaluate ISMAP or ISMAP-LIU registration against your specific buyer segment.&lt;/li&gt;
&lt;li&gt;Decide entity structure — this is a legal and tax decision, not just a go-to-market one.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Phase 4: Scale (Month 18+)
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Use the first 1–2 enterprise wins as referenceable case studies within the same industry vertical.&lt;/li&gt;
&lt;li&gt;Reassess whether Japan is the right next APAC market or whether Singapore/Australia sequencing fits your specific product better.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Cost Considerations
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cost Type&lt;/th&gt;
&lt;th&gt;What to Budget For&lt;/th&gt;
&lt;th&gt;Typical Range&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Translation-only tier&lt;/td&gt;
&lt;td&gt;Agency translation, marketing site localization&lt;/td&gt;
&lt;td&gt;$15,000–$40,000 one-time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;UX localization pass&lt;/td&gt;
&lt;td&gt;Redesign for information density, workflow fit&lt;/td&gt;
&lt;td&gt;$50,000–$150,000 one-time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JST support staffing&lt;/td&gt;
&lt;td&gt;1–2 bilingual support hires or an outsourced JP support desk&lt;/td&gt;
&lt;td&gt;$80,000–$180,000/year&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legal and contracts&lt;/td&gt;
&lt;td&gt;Japan-qualified counsel for dual-language contracts, APPI review&lt;/td&gt;
&lt;td&gt;$20,000–$60,000 one-time, plus ongoing retainer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Entity and compliance program&lt;/td&gt;
&lt;td&gt;Local entity setup, ISMAP/ISMAP-LIU pursuit if pursuing enterprise/gov&lt;/td&gt;
&lt;td&gt;$100,000–$400,000+ over 12–18 months&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;ROI signal:&lt;/strong&gt; if your average enterprise contract value in existing markets exceeds $50,000/year and you already have inbound Japan interest, the localized-launch tier typically pays back within the first two to three enterprise contracts — the full program only pays back if you're targeting large-enterprise or government accounts specifically, not general market presence.&lt;/p&gt;




&lt;h2&gt;
  
  
  Questions to Ask Your Team
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;"Can our data architecture guarantee Japan-only residency today, or is that a re-platforming project?"&lt;/li&gt;
&lt;li&gt;"Who owns the Japanese-language version of our contract, and has Japan-qualified counsel actually reviewed it?"&lt;/li&gt;
&lt;li&gt;"Is our support team staffed for JST business hours with people who read Japanese natively, or are we planning to route through translation?"&lt;/li&gt;
&lt;li&gt;"Have we budgeted the sales cycle at 12+ months, or are we still modeling it on our US/EU cycle length?"&lt;/li&gt;
&lt;li&gt;"Is this a translation-only signal test, or are we committing to the localized tier — and does everyone on the team agree which one we're doing?"&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Translation gets a foreign SaaS product into a Japanese browser. It doesn't get it past a security questionnaire, a procurement committee, or the sales cycle those groups run on. Treat localization as three separate investments (product/UX, regulatory posture, and commercial/contractual) and size the investment to the tier of buyer you're actually targeting, not to what felt sufficient in your last market. The vendors who stall in Japan usually aren't building a worse product; they're budgeting a translation-only effort against an enterprise-tier buying process.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.jetro.go.jp/en/invest/setting_up/" rel="noopener noreferrer"&gt;JETRO — Setting Up a Business in Japan&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ppc.go.jp/en/" rel="noopener noreferrer"&gt;Personal Information Protection Commission, Japan — APPI overview&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ismap.go.jp/csm?pid=top" rel="noopener noreferrer"&gt;ISMAP — Cabinet Secretariat / IPA Information System Security Management and Assessment Program&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/compliance/ismap/" rel="noopener noreferrer"&gt;AWS — ISMAP Compliance Overview&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bry Writes Code — cloud and AI infrastructure specialist, 15 years in IT, based in Tokyo. Evaluating a Japan market-entry plan for your product? &lt;a href="mailto:brywritescode@gmail.com"&gt;Let's talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>devops</category>
      <category>webdev</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Getting Started with the Claude API: Messages, Streaming, and Tool Use</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Tue, 21 Jul 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/getting-started-with-the-claude-api-messages-streaming-and-tool-use-422</link>
      <guid>https://dev.to/brywritescode/getting-started-with-the-claude-api-messages-streaming-and-tool-use-422</guid>
      <description>&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Install &lt;code&gt;anthropic&lt;/code&gt;, set &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt;, and you have a working LLM API in five lines of Python.&lt;/li&gt;
&lt;li&gt;Three models cover 95% of use cases: Opus for complex reasoning, Sonnet for most production tasks, Haiku for cheap high-volume classification.&lt;/li&gt;
&lt;li&gt;Always stream long-form responses; timeouts and bad UX come from blocking on a full response.&lt;/li&gt;
&lt;li&gt;Tool use requires a two-turn round trip: Claude signals a function call, your code runs it, you return the result. A single turn can request several tool calls at once — extracting only the first silently drops the rest.&lt;/li&gt;
&lt;li&gt;Prompt caching cuts input token costs by up to 90% for apps that send the same system prompt on every request.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Most tutorials for LLM APIs stop at "send a message, print a response." That gets you a demo. It does not get you a production application. I've integrated the Claude API into customer-facing applications and internal automation pipelines, and the same three gaps appear every time a team goes from prototype to production: they don't stream, they don't handle tool use as a round trip, and they only think about cost after the first billing cycle. The Claude API has specific patterns for streaming, tool use, and cost control that are non-obvious from a basic example — and getting them wrong results in timeouts, doubled costs, or silent logic errors when Claude tries to call a function and your code ignores it.&lt;/p&gt;

&lt;p&gt;This article covers the full picture: how the Messages API works, how to pick the right model, how to stream correctly, how to implement tool use as a complete round trip, and how to keep costs predictable at scale. All code is Python 3.11+. The companion code at &lt;code&gt;ai-integration/claude-api-quickstart/src/main.py&lt;/code&gt; is a runnable &lt;code&gt;ClaudeClient&lt;/code&gt; class that demonstrates all four patterns.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;Install the SDK and dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;anthropic python-dotenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Store your API key as an environment variable — never hardcode it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env&lt;/span&gt;
&lt;span class="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-ant-...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Anthropic SDK reads &lt;code&gt;ANTHROPIC_API_KEY&lt;/code&gt; automatically. Initialize the client once at module level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Anthropic&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# reads ANTHROPIC_API_KEY from env
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Create the client once.&lt;/strong&gt; A common mistake is instantiating &lt;code&gt;Anthropic()&lt;/code&gt; inside a request handler. The client maintains an HTTP connection pool — recreating it on every call wastes ~50ms in connection setup and adds unnecessary overhead.&lt;/p&gt;




&lt;h2&gt;
  
  
  Messages API Basics
&lt;/h2&gt;

&lt;p&gt;Every interaction with Claude goes through &lt;code&gt;client.messages.create()&lt;/code&gt;. The API uses three message roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;system&lt;/code&gt; — sets the context and rules for the entire conversation. Not a message in the history.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user&lt;/code&gt; — input from the human (or your application).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;assistant&lt;/code&gt; — Claude's previous responses. Include these to continue a multi-turn conversation.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a senior backend engineer. Be concise and direct.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What is the difference between process and thread?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Tokens used — input: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, output: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key parameters:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;model&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Required. See model selection section below.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;max_tokens&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;int&lt;/td&gt;
&lt;td&gt;Required. Upper bound on output — Claude stops here even mid-sentence.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;system&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string&lt;/td&gt;
&lt;td&gt;Optional. Sets assistant persona and rules.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;messages&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;list&lt;/td&gt;
&lt;td&gt;Required. Alternating user/assistant turns. Must start with &lt;code&gt;user&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;temperature&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;float&lt;/td&gt;
&lt;td&gt;0.0–1.0. Default 1.0. Lower = more deterministic. Use 0.0 for classification.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;stop_sequences&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;list&lt;/td&gt;
&lt;td&gt;Claude stops generating at any of these strings.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The response &lt;code&gt;stop_reason&lt;/code&gt; tells you why Claude stopped:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;end_turn&lt;/code&gt; — Claude finished naturally.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;max_tokens&lt;/code&gt; — hit the &lt;code&gt;max_tokens&lt;/code&gt; limit. Response may be truncated.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tool_use&lt;/code&gt; — Claude wants to call a function. Must handle this explicitly.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stop_sequence&lt;/code&gt; — hit one of your stop sequences.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Always check &lt;code&gt;stop_reason&lt;/code&gt;.&lt;/strong&gt; If you receive &lt;code&gt;tool_use&lt;/code&gt; and ignore it, Claude is waiting for your function result — it did not finish answering your question.&lt;/p&gt;




&lt;h2&gt;
  
  
  Model Selection
&lt;/h2&gt;

&lt;p&gt;Three production models cover nearly all use cases. Choose by task complexity and cost tolerance:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;ID&lt;/th&gt;
&lt;th&gt;Best For&lt;/th&gt;
&lt;th&gt;Not For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude Opus 4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude-opus-4-8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Complex multi-step reasoning, code generation from ambiguous specs, research synthesis&lt;/td&gt;
&lt;td&gt;High-volume tasks (cost), simple classification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude Sonnet 4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude-sonnet-4-6&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Most production API tasks, customer-facing chat, structured extraction&lt;/td&gt;
&lt;td&gt;Tasks where you need the cheapest option&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Claude Haiku 4&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;claude-haiku-4-5-20251001&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Classification, summarization, high-volume extraction, quick one-turn answers&lt;/td&gt;
&lt;td&gt;Multi-step reasoning, long-form generation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fncohqigmz2e23e3akx33.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fncohqigmz2e23e3akx33.png" alt="Model Selection" width="800" height="1472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Default to &lt;code&gt;claude-sonnet-4-6&lt;/code&gt; for any new feature. I start every integration on Sonnet and only swap to Haiku after running a sample of real production inputs through both models and comparing outputs — not benchmarks, actual outputs for your specific task. Migrate to Haiku only after confirming the output quality is acceptable for that specific task.&lt;/p&gt;




&lt;h2&gt;
  
  
  Streaming Responses
&lt;/h2&gt;

&lt;p&gt;For responses longer than a paragraph, stream. A non-streaming call for a 2,000-token response waits 10–30 seconds before returning anything to the user. Streaming begins returning tokens in ~200ms.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stream_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text_stream&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# newline after stream ends
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;with client.messages.stream()&lt;/code&gt; context manager handles connection cleanup. Inside the block, &lt;code&gt;stream.text_stream&lt;/code&gt; yields each text delta as Claude generates it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When to stream:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any user-facing response longer than ~100 tokens.&lt;/li&gt;
&lt;li&gt;Document generation, code generation, long explanations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When not to stream:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Classification tasks where you need the complete label before doing anything.&lt;/li&gt;
&lt;li&gt;Tool use — stream the full round trip only if you handle partial tool input JSON.&lt;/li&gt;
&lt;li&gt;Batch processing jobs where individual response latency does not matter.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In practice, I default to streaming for anything user-facing and non-streaming for anything running in a background job. The distinction matters because mixing the two in the same codebase without clear conventions leads to subtle bugs when someone reuses a non-streaming helper in a latency-sensitive context.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1mfiwdk7oipw6le0mory.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1mfiwdk7oipw6le0mory.png" alt="Streaming Responses" width="800" height="656"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Tool Use (Function Calling)
&lt;/h2&gt;

&lt;p&gt;Tool use is a two-turn exchange. In turn one, Claude signals which function to call and with what arguments. Your application executes the function. In turn two, you send the result back, and Claude incorporates it into its final answer.&lt;/p&gt;

&lt;p&gt;Turn one is not guaranteed to hold a single tool call. When a task decomposes into independent lookups, Claude returns multiple &lt;code&gt;tool_use&lt;/code&gt; blocks in one &lt;code&gt;content&lt;/code&gt; array against a single &lt;code&gt;stop_reason: tool_use&lt;/code&gt;. Code that grabs "the" tool call with &lt;code&gt;next(...)&lt;/code&gt; runs the first and silently drops the others; Claude then sees a &lt;code&gt;tool_result&lt;/code&gt; for a call it never asked to have answered, and the response degrades in ways that don't throw an exception, so they slip through code review and manual testing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Define a tool
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;CALCULATOR_TOOLS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;calculator&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Performs basic arithmetic. Use when the user asks for a calculation.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;input_schema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;operation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enum&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;add&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subtract&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;multiply&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;divide&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The arithmetic operation to perform.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;First operand.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;number&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Second operand.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;operation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Handle the round trip
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;concurrent.futures&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;use_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;

    &lt;span class="c1"&gt;# Turn 1: Claude may request one or more tool calls
&lt;/span&gt;    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CALCULATOR_TOOLS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop_reason&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Claude answered directly — no tool call needed
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;

    &lt;span class="c1"&gt;# Collect every tool_use block — a single turn can carry more than one
&lt;/span&gt;    &lt;span class="n"&gt;tool_use_blocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Execute independent calculator calls concurrently; each is a pure,
&lt;/span&gt;    &lt;span class="c1"&gt;# side-effect-free function, so order doesn't matter and running them
&lt;/span&gt;    &lt;span class="c1"&gt;# sequentially would just add latency for no benefit.
&lt;/span&gt;    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_use_blocks&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;_run_calculator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;tool_use_blocks&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Turn 2: one tool_result per tool_use_id, all in the same user message
&lt;/span&gt;    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tool_use_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_use_blocks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="n"&gt;final&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;CALCULATOR_TOOLS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;final&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_run_calculator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;operation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;add&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subtract&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;multiply&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;divide&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Division by zero&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown operation: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp18slsbg2rta2uqec6oi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp18slsbg2rta2uqec6oi.png" alt="Handle the Roundtrip" width="800" height="303"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The key detail: &lt;code&gt;messages.append({"role": "assistant", "content": response.content})&lt;/code&gt; — you must include Claude's entire response object (including the tool use block) in the history before sending the tool result. Sending only the text content breaks the conversation. I've seen this cause a &lt;code&gt;400 BadRequestError&lt;/code&gt; in production because a developer extracted just the text from the assistant response and discarded the tool use block before appending — the API rejects the resulting message history as structurally invalid, and the error message doesn't point to the exact field.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parallel versus sequential execution.&lt;/strong&gt; Whether to run multiple tool calls concurrently is a decision about the tools, not the API — the API only requires that every &lt;code&gt;tool_use_id&lt;/code&gt; from turn one gets a matching &lt;code&gt;tool_result&lt;/code&gt; in turn two, in any order. Run them concurrently (thread pool for blocking I/O, &lt;code&gt;asyncio.gather&lt;/code&gt; for async tools) when the calls are independent and side-effect-free, as with the calculator above; that's where the latency win is real, since three tool calls at 200ms each is 600ms sequential versus ~200ms concurrent. Run them sequentially, deliberately, when a later call depends on an earlier one's result, when the tools share a rate-limited resource, or when execution order matters for a side effect (a write followed by a read of what was just written). Defaulting to concurrency for tools that aren't actually independent is how you get race conditions that only show up under load.&lt;/p&gt;




&lt;h2&gt;
  
  
  Error Handling and Retries
&lt;/h2&gt;

&lt;p&gt;Rate limit errors and transient server errors are inevitable at any meaningful scale. I've seen a well-tested integration start hitting &lt;code&gt;RateLimitError&lt;/code&gt; the week after launch simply because a new feature tripled the request volume — the app had no retry logic, and every error surfaced directly to the user as a 500. Implement exponential backoff from day one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_create_with_retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;types&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;kwargs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;system&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RateLimitError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;
            &lt;span class="n"&gt;wait&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;  &lt;span class="c1"&gt;# 1s, 2s, 4s
&lt;/span&gt;            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;APIStatusError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common exceptions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Exception&lt;/th&gt;
&lt;th&gt;Cause&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;anthropic.RateLimitError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Too many requests&lt;/td&gt;
&lt;td&gt;Exponential backoff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;anthropic.APIStatusError&lt;/code&gt; (5xx)&lt;/td&gt;
&lt;td&gt;Server error&lt;/td&gt;
&lt;td&gt;Retry with backoff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;anthropic.BadRequestError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Invalid request (context too long, invalid params)&lt;/td&gt;
&lt;td&gt;Do not retry — fix the request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;anthropic.AuthenticationError&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Bad API key&lt;/td&gt;
&lt;td&gt;Do not retry — check env var&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Check token count before sending a long conversation to avoid &lt;code&gt;BadRequestError&lt;/code&gt; on context overflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;token_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count_tokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Prompt uses &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;token_count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tokens&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Cost Management and Prompt Caching
&lt;/h2&gt;

&lt;p&gt;Claude charges per token: input tokens (what you send) and output tokens (what Claude generates). Output tokens cost 3–5× more per token than input tokens. The usage breakdown appears in every response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;input_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# tokens you sent
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# tokens Claude generated
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Prompt caching
&lt;/h3&gt;

&lt;p&gt;If your application sends the same system prompt on every request (a common pattern), prompt caching can reduce input costs by 60–90%. Mark the cacheable prefix with &lt;code&gt;cache_control&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claude-sonnet-4-6&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a senior backend engineer...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;[very long system prompt]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cache_control&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ephemeral&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_question&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the first call, Anthropic caches the marked prefix. Subsequent calls within the cache TTL pay ~10% of the normal input price for cached tokens. The response includes cache statistics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache_creation_input_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# tokens written to cache
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache_read_input_tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;# tokens read from cache (cheap)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;When caching pays off:&lt;/strong&gt; system prompts over ~1,000 tokens with more than a few calls per day. I enable caching by default on any app with a fixed system prompt longer than 500 tokens — the implementation cost is two lines and the savings compound quickly once you're past a few hundred daily active users. For shorter prompts or low-volume applications, the overhead is negligible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Not streaming long-form responses.&lt;/strong&gt; A blocking call for a 2,000-token response times out in some HTTP clients and always delivers a poor user experience. Use &lt;code&gt;client.messages.stream()&lt;/code&gt; for any response that takes more than ~1 second.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Ignoring &lt;code&gt;stop_reason&lt;/code&gt;.&lt;/strong&gt; If &lt;code&gt;stop_reason&lt;/code&gt; is &lt;code&gt;tool_use&lt;/code&gt; and you return &lt;code&gt;response.content[0].text&lt;/code&gt;, you are returning a partial or empty string; Claude was waiting to give you a real answer after executing the tool. I've seen this ship to production as a bug where the API "randomly" returned empty responses on certain questions; questions that happened to trigger tool use. Always check &lt;code&gt;stop_reason&lt;/code&gt; before extracting text.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Extracting only the first &lt;code&gt;tool_use&lt;/code&gt; block.&lt;/strong&gt; &lt;code&gt;next(b for b in response.content if b.type == "tool_use")&lt;/code&gt; handles the common case and silently breaks the moment Claude returns two or three tool calls in one turn — the rest are dropped, and there's no exception to point you at the bug. Iterate &lt;code&gt;response.content&lt;/code&gt; for all &lt;code&gt;tool_use&lt;/code&gt; blocks and return one &lt;code&gt;tool_result&lt;/code&gt; per &lt;code&gt;tool_use_id&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Setting &lt;code&gt;max_tokens&lt;/code&gt; too low.&lt;/strong&gt; If &lt;code&gt;max_tokens&lt;/code&gt; is smaller than the full response, Claude stops mid-sentence and &lt;code&gt;stop_reason&lt;/code&gt; returns &lt;code&gt;max_tokens&lt;/code&gt;. The response is valid JSON but the content is truncated. Set &lt;code&gt;max_tokens&lt;/code&gt; generously; you are not charged for unused headroom.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Recreating the &lt;code&gt;Anthropic()&lt;/code&gt; client on every request.&lt;/strong&gt; The client maintains an HTTP connection pool internally. Instantiating it per-request wastes connection setup time and bypasses the pool. Create one instance at module level and reuse it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sending unbounded conversation history.&lt;/strong&gt; Every previous turn counts toward the input token cost. For long conversations, implement a windowing strategy: keep the system prompt, the last N turns, and trim the middle. Use &lt;code&gt;client.messages.count_tokens()&lt;/code&gt; before every send to catch overruns early.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Full Example
&lt;/h2&gt;

&lt;p&gt;The companion code at &lt;code&gt;ai-integration/claude-api-quickstart/src/main.py&lt;/code&gt; implements all patterns above as a &lt;code&gt;ClaudeClient&lt;/code&gt; class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;src.main&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ClaudeClient&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;claude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ClaudeClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Basic message
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a concise technical explainer.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What is a vector embedding in one sentence?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Streaming
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stream_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;system&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a Python expert.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Explain Python generators in detail.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flush&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Tool use
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claude&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use_tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What is 2847 multiplied by 193?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;ai-integration/claude-api-quickstart
python &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate  &lt;span class="c"&gt;# Windows: .venv\Scripts\activate&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env       &lt;span class="c"&gt;# add your key&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; src.main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Full source: &lt;a href="https://github.com/brywritescode/bry-writes-code-examples.git" rel="noopener noreferrer"&gt;GitHub link&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  State: The Conversation Turn Model
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvfn9nkb858jymy0ilzr7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvfn9nkb858jymy0ilzr7.png" alt="The Conversation Turn Model" width="799" height="763"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The difference between a Claude integration that works in a demo and one that holds up in production is not the model — it's whether you stream, whether you handle tool use as a complete round trip, and whether you have a cost strategy before your usage scales. Most production failures I've traced come back to one of those three. Ship an integration that gets all three right from the start, and you will spend your time building features instead of debugging silent errors and explaining surprise billing cycles to stakeholders.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.anthropic.com/en/api/messages" rel="noopener noreferrer"&gt;Anthropic Messages API Reference&lt;/a&gt; — complete parameter documentation&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/tool-use" rel="noopener noreferrer"&gt;Tool Use Guide&lt;/a&gt; — multi-tool, parallel tool use, error handling&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching" rel="noopener noreferrer"&gt;Prompt Caching&lt;/a&gt; — cache_control details and cache TTL&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.anthropic.com/en/api/messages-streaming" rel="noopener noreferrer"&gt;Streaming Messages&lt;/a&gt; — raw SSE event format&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://docs.anthropic.com/en/docs/about-claude/models/overview" rel="noopener noreferrer"&gt;Model Overview&lt;/a&gt; — current model IDs and specifications&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bry Writes Code — cloud and AI infrastructure specialist. Building with Claude API? &lt;a href="mailto:brywritescode@gmail.com"&gt;Let's talk&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>claude</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Amazon API Gateway: REST vs HTTP APIs, Explained via the AWS CLI</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Tue, 14 Jul 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/amazon-api-gateway-rest-vs-http-apis-explained-via-the-aws-cli-3lam</link>
      <guid>https://dev.to/brywritescode/amazon-api-gateway-rest-vs-http-apis-explained-via-the-aws-cli-3lam</guid>
      <description>&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;REST API and HTTP API are two separate products under one brand name, with two separate CLI command namespaces — &lt;code&gt;aws apigateway&lt;/code&gt; and &lt;code&gt;aws apigatewayv2&lt;/code&gt;. They are not versions of each other.&lt;/li&gt;
&lt;li&gt;HTTP API costs $1.00 per million calls versus REST API's flat $3.50 per million — a 71% difference that compounds fast at scale.&lt;/li&gt;
&lt;li&gt;Pick REST API only when you need something HTTP API structurally cannot do: API keys, per-client usage plans, AWS WAF, request validation, or private (VPC-only) endpoints.&lt;/li&gt;
&lt;li&gt;The CLI reveals the real gap between them better than any feature table: building the same Lambda-backed endpoint takes one command on HTTP API and five on REST API.&lt;/li&gt;
&lt;li&gt;Tear down what you build. Both products bill for idle resources, and orphaned stages are the most common line item nobody remembers creating.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CLI/SDK version tested against: &lt;code&gt;aws-cli/2.35.x&lt;/code&gt; (current stable as of this writing — the &lt;code&gt;apigatewayv2&lt;/code&gt; and &lt;code&gt;apigateway&lt;/code&gt; command sets are stable across 2.x minor releases, but always run &lt;code&gt;aws --version&lt;/code&gt; before following along)&lt;/li&gt;
&lt;li&gt;An AWS account with an IAM user or role that has &lt;code&gt;apigateway:*&lt;/code&gt; and &lt;code&gt;lambda:*&lt;/code&gt; permissions, and a default region configured (&lt;code&gt;aws configure&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;A deployed Lambda function to integrate against — the examples below use a function named &lt;code&gt;user-lookup&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Every AWS API Gateway tutorial I've read picks a product on page one and never explains why. Half of them build a REST API with &lt;code&gt;create-resource&lt;/code&gt; and &lt;code&gt;put-method&lt;/code&gt; chains that look like plumbing. The other half quick-create an HTTP API in one line and call it a day. Neither tells you when the other approach would have been the better call — and I've watched teams build a REST API out of habit, hit the $3.50-per-million bill at scale, and only then discover HTTP API existed.&lt;/p&gt;

&lt;p&gt;The two products share a brand name and almost nothing else under the hood. They have separate CLI command namespaces, separate feature sets, and a 3.5x price difference that AWS designed on purpose, not as an afterthought. Understanding which one you're actually building — and why — matters more than memorizing either command sequence.&lt;/p&gt;

&lt;p&gt;This article builds the identical Lambda-backed endpoint twice: once on HTTP API, once on REST API, entirely from the AWS CLI. You'll see where the commands diverge, where they can't diverge because one product simply lacks the feature, and which one to default to for your next project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two Products, Not Two Versions
&lt;/h2&gt;

&lt;p&gt;AWS API Gateway is a brand covering three products: REST API, HTTP API, and WebSocket API. REST API launched in 2015. HTTP API launched in 2019 as a deliberately smaller product — AWS's own announcement framed it as coverage for the roughly 80% of REST API customers who only needed Lambda proxy integration, JWT auth, and CORS, and were paying REST API prices for features they never touched.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3sdu7bitnt0rpuj3lhnv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3sdu7bitnt0rpuj3lhnv.png" alt="Two Products, Not Two Versions" width="784" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: One brand, two CLI namespaces, non-overlapping feature sets.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That last point trips people up the most: &lt;code&gt;aws apigateway&lt;/code&gt; and &lt;code&gt;aws apigatewayv2&lt;/code&gt; are not "v1 and v2 of the same thing" the way S3's SDK versions are. They are two independent command trees. A script written for REST API will not run against HTTP API with a find-and-replace.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building the Same Endpoint on HTTP API
&lt;/h2&gt;

&lt;p&gt;Start with HTTP API, because it's shorter and shows what "quick create" actually collapses into one call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Quick-create an HTTP API backed directly by a Lambda function.&lt;/span&gt;
&lt;span class="c"&gt;# This single command creates the API, a default catch-all route,&lt;/span&gt;
&lt;span class="c"&gt;# a Lambda integration, and an auto-deploying default stage.&lt;/span&gt;
aws apigatewayv2 create-api &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; user-lookup-http &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--protocol-type&lt;/span&gt; HTTP &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:lambda:us-east-1:123456789012:function:user-lookup"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one call is doing the work of five REST API calls, which is the point. Quick create only works for a single catch-all route. The moment you need more than one route with different backends, you build it up explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Explicit build — needed once you have more than one route.&lt;/span&gt;
&lt;span class="nv"&gt;API_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws apigatewayv2 create-api &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; user-lookup-http &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--protocol-type&lt;/span&gt; HTTP &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'ApiId'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;INTEGRATION_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws apigatewayv2 create-integration &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--api-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--integration-type&lt;/span&gt; AWS_PROXY &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--integration-uri&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:lambda:us-east-1:123456789012:function:user-lookup"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--payload-format-version&lt;/span&gt; &lt;span class="s2"&gt;"2.0"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'IntegrationId'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

aws apigatewayv2 create-route &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--api-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--route-key&lt;/span&gt; &lt;span class="s2"&gt;"GET /users/{userId}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--target&lt;/span&gt; &lt;span class="s2"&gt;"integrations/&lt;/span&gt;&lt;span class="nv"&gt;$INTEGRATION_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# HTTP API stages auto-deploy by default — no separate deployment call needed&lt;/span&gt;
aws apigatewayv2 create-stage &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--api-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stage-name&lt;/span&gt; &lt;span class="s1"&gt;'$default'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--auto-deploy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four commands, no separate deployment step because &lt;code&gt;$default&lt;/code&gt; auto-deploys on every change. Add a JWT authorizer — the feature HTTP API supports natively and REST API cannot without a Lambda authorizer standing in for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws apigatewayv2 create-authorizer &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--api-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--authorizer-type&lt;/span&gt; JWT &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--identity-source&lt;/span&gt; &lt;span class="s1"&gt;'$request.header.Authorization'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; user-pool-jwt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--jwt-configuration&lt;/span&gt; &lt;span class="s1"&gt;'{"Audience":["https://api.example.com"],"Issuer":"https://cognito-idp.us-east-1.amazonaws.com/us-east-1_XXXXXXXXX"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Grant API Gateway permission to invoke the Lambda, and the endpoint is live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws lambda add-permission &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--function-name&lt;/span&gt; user-lookup &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--statement-id&lt;/span&gt; apigw-http-invoke &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--action&lt;/span&gt; lambda:InvokeFunction &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--principal&lt;/span&gt; apigateway.amazonaws.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source-arn&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:execute-api:us-east-1:123456789012:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;API_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/*/*/users/*"&lt;/span&gt;

curl &lt;span class="s2"&gt;"https://&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;API_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.execute-api.us-east-1.amazonaws.com/users/usr_42"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Total: five commands from zero to a JWT-protected, auto-deploying endpoint.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building the Same Endpoint on REST API
&lt;/h2&gt;

&lt;p&gt;Now the same endpoint on REST API. Structurally, REST API models resources as a tree — you create the API, then walk down from the root resource creating child resources, then attach an HTTP method and integration to each resource individually.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvipjg2iky27zteb6sxwp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvipjg2iky27zteb6sxwp.png" alt="Building the Same Endpoint on REST API" width="784" height="561"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: REST API's resource-tree model requires an explicit deployment step that HTTP API's auto-deploy stages skip entirely.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;API_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws apigateway create-rest-api &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; user-lookup-rest &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--endpoint-configuration&lt;/span&gt; &lt;span class="nv"&gt;types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;REGIONAL &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;ROOT_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws apigateway get-resources &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--rest-api-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'items[?path==`/`].id'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;USERS_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws apigateway create-resource &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--rest-api-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parent-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ROOT_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--path-part&lt;/span&gt; &lt;span class="nb"&gt;users&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;USERID_RESOURCE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws apigateway create-resource &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--rest-api-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--parent-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USERS_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--path-part&lt;/span&gt; &lt;span class="s1"&gt;'{userId}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'id'&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

aws apigateway put-method &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--rest-api-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USERID_RESOURCE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--http-method&lt;/span&gt; GET &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--authorization-type&lt;/span&gt; NONE

aws apigateway put-integration &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--rest-api-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$USERID_RESOURCE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--http-method&lt;/span&gt; GET &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--type&lt;/span&gt; AWS_PROXY &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--integration-http-method&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--uri&lt;/span&gt; &lt;span class="s2"&gt;"arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:user-lookup/invocations"&lt;/span&gt;

&lt;span class="c"&gt;# REST API has no auto-deploy — every change requires an explicit deployment&lt;/span&gt;
aws apigateway create-deployment &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--rest-api-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--stage-name&lt;/span&gt; prod
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven commands versus HTTP API's five, and that's before adding auth — REST API has no native JWT authorizer at all. You'd add a Lambda authorizer via &lt;code&gt;create-authorizer --type TOKEN&lt;/code&gt;, which is its own Lambda function to write, deploy, and maintain, purely to validate a token that HTTP API checks natively with one &lt;code&gt;--jwt-configuration&lt;/code&gt; flag. &lt;code&gt;AUTHORIZATION_TYPE=NONE&lt;/code&gt; above is intentionally the simplest case to keep the comparison fair — swapping in Cognito or a Lambda authorizer adds two more commands on the REST API side and zero on the HTTP API side.&lt;/p&gt;

&lt;p&gt;That gap is the entire argument for defaulting to HTTP API. Not the price alone — the operational surface. Every additional resource, method, and integration on REST API is something you version, test, and can misconfigure. HTTP API's route-based model has fewer places to get it wrong.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Feature Gap That Actually Justifies REST API
&lt;/h2&gt;

&lt;p&gt;None of this means REST API is obsolete. It means most projects don't need what it offers. Per AWS's own comparison documentation, REST API supports several things HTTP API structurally cannot do at all:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;REST API&lt;/th&gt;
&lt;th&gt;HTTP API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;API keys + usage plans&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-client rate limiting&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AWS WAF integration&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request validation (schema)&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private (VPC-only) endpoints&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Response caching&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mock integrations&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JWT authorizer (native)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto-deploying stages&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Private integrations via AWS Cloud Map&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you're selling API access to third-party developers and need to issue API keys with per-key quotas, that's REST API, full stop — HTTP API has no equivalent mechanism, not a smaller version of one. Same for WAF: if compliance requires a web application firewall in front of the gateway, REST API is your only option between the two. I've built exactly one REST API in the last two years for a client billing external partners per API key against a usage plan — every other new project defaulted to HTTP API.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost at Scale
&lt;/h2&gt;

&lt;p&gt;The sticker prices understate how fast this compounds. At 500 million requests a month:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;HTTP API&lt;/th&gt;
&lt;th&gt;REST API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First 300M&lt;/td&gt;
&lt;td&gt;$300 (at $1.00/M)&lt;/td&gt;
&lt;td&gt;$1,050 (at $3.50/M)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Remaining 200M&lt;/td&gt;
&lt;td&gt;$180 (at $0.90/M)&lt;/td&gt;
&lt;td&gt;$700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Monthly total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$480&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$1,750&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's before data transfer, which is identical on both ($0.09/GB after the first 100 GB). A team that defaults to REST API out of habit on a 500M-request workload is paying an extra $1,270 a month for features it may never touch. Multiply that by however many services in your org made the same default choice and it stops being a rounding error.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1: Choosing REST API because older tutorials only cover it&lt;/strong&gt;&lt;br&gt;
Most CLI tutorials predate HTTP API's 2019 launch or were never updated. If a walkthrough uses &lt;code&gt;aws apigateway create-rest-api&lt;/code&gt; and never mentions &lt;code&gt;apigatewayv2&lt;/code&gt;, it's not wrong, just incomplete — check whether you actually need REST API's feature set before copying it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2: Assuming &lt;code&gt;apigatewayv2&lt;/code&gt; supports API keys&lt;/strong&gt;&lt;br&gt;
It doesn't, and there's no workaround at the gateway layer. Teams that need lightweight per-client identification on HTTP API end up implementing it themselves — a custom header checked in the Lambda, or a JWT claim — rather than getting it for free the way REST API's usage plans provide it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3: Forgetting the deployment step on REST API&lt;/strong&gt;&lt;br&gt;
Unlike HTTP API's auto-deploying &lt;code&gt;$default&lt;/code&gt; stage, REST API changes are invisible until you run &lt;code&gt;create-deployment&lt;/code&gt;. I've debugged more than one "my change isn't showing up" ticket that was just a missing deployment call after a &lt;code&gt;put-integration&lt;/code&gt; update.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 4: Leaving orphaned APIs after testing&lt;/strong&gt;&lt;br&gt;
Both products bill nothing for an idle API with zero traffic, but Lambda &lt;code&gt;add-permission&lt;/code&gt; grants and unused custom domain mappings pile up and become a mess to audit six months later. Clean up as you go.&lt;/p&gt;


&lt;h2&gt;
  
  
  Teardown
&lt;/h2&gt;

&lt;p&gt;Both products are cheap to tear down completely, and you should — leftover test APIs are how AWS bills quietly creep up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# HTTP API&lt;/span&gt;
aws apigatewayv2 delete-api &lt;span class="nt"&gt;--api-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# REST API&lt;/span&gt;
aws apigateway delete-rest-api &lt;span class="nt"&gt;--rest-api-id&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$API_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full teardown script, including the Lambda permission cleanup, is in the companion repo below.&lt;/p&gt;




&lt;h2&gt;
  
  
  Production Considerations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Performance:&lt;/strong&gt; HTTP API adds roughly 6 ms of median gateway overhead; REST API adds roughly 11 ms. At p99, the gap narrows because both are dominated by backend latency, not gateway overhead — don't over-index on this number when choosing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security:&lt;/strong&gt; If you're on HTTP API and need WAF-equivalent protection, put CloudFront with AWS WAF in front of it — HTTP API supports CloudFront origins natively. This is the standard workaround and it works well, but it's an extra piece of infrastructure REST API gives you natively.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cost:&lt;/strong&gt; Re-run the math above whenever traffic grows 5-10x. A choice that made sense at 10M requests a month can look very different at 500M.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitoring:&lt;/strong&gt; Both emit CloudWatch metrics. Only REST API emits execution logs and X-Ray traces natively — on HTTP API, instrument tracing inside the Lambda itself if you need request-level tracing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full Example: Build-and-Verify Script
&lt;/h2&gt;

&lt;p&gt;The companion repository includes a complete bash script that builds both APIs, verifies each with a live &lt;code&gt;curl&lt;/code&gt;, and tears both down. The core verification logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-euo&lt;/span&gt; pipefail

verify_endpoint&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;status
  &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s1"&gt;'%{http_code}'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"200"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"OK   &lt;/span&gt;&lt;span class="nv"&gt;$label&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL &lt;/span&gt;&lt;span class="nv"&gt;$label&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
  &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

verify_endpoint &lt;span class="s2"&gt;"https://&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HTTP_API_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.execute-api.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;AWS_REGION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.amazonaws.com/users/usr_42"&lt;/span&gt; &lt;span class="s2"&gt;"HTTP API"&lt;/span&gt;
verify_endpoint &lt;span class="s2"&gt;"https://&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REST_API_ID&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.execute-api.&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;AWS_REGION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.amazonaws.com/prod/users/usr_42"&lt;/span&gt; &lt;span class="s2"&gt;"REST API"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Full source with setup, teardown, and verification scripts: &lt;a href="https://github.com/brywritescode/bry-writes-code-examples.git" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; → &lt;code&gt;cloud-apis/amazon-api-gateway-cli/&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Default to HTTP API. It's cheaper, has fewer commands to build and maintain, and natively supports JWT auth — the thing most new services actually need. Reach for REST API only when you specifically need API keys with usage plans, WAF, request validation, caching, or private VPC-only endpoints — features HTTP API doesn't have a smaller version of, because it was never built to. The CLI makes this concrete in a way the marketing pages don't: five commands versus seven isn't just fewer keystrokes, it's fewer places for a misconfigured resource or a forgotten deployment to bite you in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-vs-rest.html" rel="noopener noreferrer"&gt;Choose between REST APIs and HTTP APIs — Amazon API Gateway Developer Guide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/api-gateway/pricing/" rel="noopener noreferrer"&gt;Amazon API Gateway Pricing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cli/latest/reference/apigatewayv2/" rel="noopener noreferrer"&gt;apigatewayv2 — AWS CLI Command Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/cli/latest/reference/apigateway/" rel="noopener noreferrer"&gt;apigateway — AWS CLI Command Reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/blogs/compute/announcing-http-apis-for-amazon-api-gateway/" rel="noopener noreferrer"&gt;Announcing HTTP APIs for Amazon API Gateway — AWS Compute Blog&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bry Writes Code — cloud and API infrastructure specialist. Migrating a REST API to HTTP API, or deciding which one to build next? &lt;a href="mailto:brywritescode@gmail.com"&gt;Get in touch&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>aws</category>
      <category>typescript</category>
      <category>backend</category>
    </item>
    <item>
      <title>REST API Design Best Practices: A Field Guide for Backend Developers</title>
      <dc:creator>Bry</dc:creator>
      <pubDate>Tue, 07 Jul 2026 15:00:00 +0000</pubDate>
      <link>https://dev.to/brywritescode/rest-api-design-best-practices-a-field-guide-for-backend-developers-2548</link>
      <guid>https://dev.to/brywritescode/rest-api-design-best-practices-a-field-guide-for-backend-developers-2548</guid>
      <description>&lt;h2&gt;
  
  
  Key Points
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Name resources with &lt;strong&gt;nouns and plural forms&lt;/strong&gt; (&lt;code&gt;/users&lt;/code&gt;, &lt;code&gt;/orders/{id}&lt;/code&gt;), never verbs (&lt;code&gt;/getUser&lt;/code&gt;, &lt;code&gt;/createOrder&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Match HTTP methods to their &lt;strong&gt;RFC-defined semantics&lt;/strong&gt; — GET is safe and idempotent, POST is neither, PATCH is partial update.&lt;/li&gt;
&lt;li&gt;Return &lt;strong&gt;RFC 7807 Problem Details&lt;/strong&gt; for all error responses — a consistent, machine-readable error shape your consumers can rely on.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;URL path versioning&lt;/strong&gt; (&lt;code&gt;/v1/&lt;/code&gt;) — it is visible, cacheable, and debuggable without header inspection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cursor-based pagination&lt;/strong&gt; scales to any dataset size; offset-based pagination breaks under concurrent writes.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;An API that ignores HTTP semantics, uses verbs in URLs, and returns &lt;code&gt;{ "error": "something went wrong" }&lt;/code&gt; is technically functional. It also generates a steady stream of support tickets, misconfigured clients, and integration bugs that consume engineering time for as long as the API exists.&lt;/p&gt;

&lt;p&gt;REST is not a standard — it is a set of architectural constraints described by Roy Fielding in 2000. Most APIs that call themselves RESTful violate several of those constraints and still work fine. The practices in this article are not about philosophical purity. They are about making choices that reduce friction for the developers consuming your API, reduce bugs at integration points, and reduce maintenance burden over a long API lifetime.&lt;/p&gt;

&lt;p&gt;I've shipped and maintained REST APIs across enough companies to know that the decisions you make in week one are the ones you're still apologizing for in year three. This article is the set of defaults I'd apply if I were starting a new API today — with a reference implementation that shows all of them working together.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffinekl21alc2galvvbu8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffinekl21alc2galvvbu8.png" alt="Architecture Overview" width="800" height="141"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Diagram: Request flow from client through API Gateway, token validation, business logic, and data layer.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Resource Design: Naming and Hierarchy
&lt;/h2&gt;

&lt;p&gt;Resources are the nouns in your API. Every URL identifies a resource, not an action.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule:&lt;/strong&gt; Use plural nouns for collections, singular for specific members.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET    /v1/articles          → list all articles
POST   /v1/articles          → create an article
GET    /v1/articles/{id}     → get one article
PATCH  /v1/articles/{id}     → partially update one article
DELETE /v1/articles/{id}     → delete one article
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Never use verbs in paths:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ /v1/getArticle
❌ /v1/createArticle
❌ /v1/articles/delete
✓  /v1/articles/{id}   with DELETE method
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For sub-resources, express ownership through hierarchy — but stop at two levels. Deeper than two levels signals the resource model needs rethinking.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /v1/articles/{id}/comments      ← acceptable
GET /v1/articles/{id}/comments/{cid}/replies/{rid}  ← too deep, model differently
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Resource Data Model
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff763tktp1lw9ey7ysyju.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff763tktp1lw9ey7ysyju.png" alt="Resource Data Model" width="628" height="1624"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  HTTP Methods and Status Codes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Method Semantics
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Safe&lt;/th&gt;
&lt;th&gt;Idempotent&lt;/th&gt;
&lt;th&gt;Body&lt;/th&gt;
&lt;th&gt;When to use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Fetch a resource or collection. Never mutate.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Create a new resource. Each call may produce a new resource.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PUT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Replace a resource completely. Must send the full representation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PATCH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Partial update. Send only the fields to change.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;DELETE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Remove a resource. Idempotent — deleting twice is not an error.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Safe&lt;/strong&gt; means the method must not change server state.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Idempotent&lt;/strong&gt; means calling it N times has the same effect as calling it once.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;PATCH&lt;/code&gt; over &lt;code&gt;PUT&lt;/code&gt; for updates unless you genuinely need full replacement. Requiring clients to send the entire resource for a title change is wasteful and error-prone.&lt;/p&gt;
&lt;h3&gt;
  
  
  Status Codes That Matter
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Code&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;200 OK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Success with body&lt;/td&gt;
&lt;td&gt;GET, PATCH, PUT responses&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;201 Created&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Resource created&lt;/td&gt;
&lt;td&gt;POST success — include &lt;code&gt;Location&lt;/code&gt; header&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;204 No Content&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Success, no body&lt;/td&gt;
&lt;td&gt;DELETE success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;400 Bad Request&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Malformed request&lt;/td&gt;
&lt;td&gt;Missing required field, invalid format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;401 Unauthorized&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Not authenticated&lt;/td&gt;
&lt;td&gt;No token or invalid token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;403 Forbidden&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Authenticated but not allowed&lt;/td&gt;
&lt;td&gt;Correct token, wrong permissions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;404 Not Found&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Resource does not exist&lt;/td&gt;
&lt;td&gt;ID not in database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;409 Conflict&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;State conflict&lt;/td&gt;
&lt;td&gt;Duplicate create, optimistic lock failure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;422 Unprocessable Entity&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Valid format, invalid content&lt;/td&gt;
&lt;td&gt;Passes JSON parse but fails business validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;429 Too Many Requests&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rate limit hit&lt;/td&gt;
&lt;td&gt;Include &lt;code&gt;Retry-After&lt;/code&gt; header&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;500 Internal Server Error&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Server bug&lt;/td&gt;
&lt;td&gt;Never expose stack traces&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not return &lt;code&gt;200 OK&lt;/code&gt; with an error payload. A client that checks the status code first — which all good clients do — will miss your error.&lt;/p&gt;
&lt;h3&gt;
  
  
  Request-Response Sequence
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffkvo851cirp2zuk5kwn1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffkvo851cirp2zuk5kwn1.png" alt="Request Response Sequence" width="800" height="660"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Response Shape: Data and Errors
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Success Response
&lt;/h3&gt;

&lt;p&gt;Keep success responses flat when possible. Wrap in a &lt;code&gt;data&lt;/code&gt; envelope only when you need top-level metadata alongside the resource (e.g., pagination).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Single&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;no&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;wrapper&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;needed&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"REST API Design Best Practices"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authorId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"usr_789"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"published"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"createdAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-01T09:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"updatedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-10-01T09:00:00Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Collection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;wrapper&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;needed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;pagination&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;metadata&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pagination"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"limit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"next_cursor"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc124"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;a href="https://www.iso.org/iso-8601-date-and-time-format.html" rel="noopener noreferrer"&gt;ISO 8601&lt;/a&gt; for all timestamps with UTC timezone (&lt;code&gt;Z&lt;/code&gt; suffix). Never return Unix epoch integers — they require client-side conversion and obscure the value during debugging.&lt;/p&gt;

&lt;h3&gt;
  
  
  Error Response — RFC 7807 Problem Details
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.rfc-editor.org/rfc/rfc7807" rel="noopener noreferrer"&gt;RFC 7807&lt;/a&gt; defines a standard error response format. Use it. Your consumers can write error handling once and it works across all your endpoints.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://api.example.com/errors/not-found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Article Not Found"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"No article with id 'abc999' exists."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"instance"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/v1/articles/abc999"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set &lt;code&gt;Content-Type: application/problem+json&lt;/code&gt; on all error responses.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://api.example.com/errors/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/problem+json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage&lt;/span&gt;
&lt;span class="nf"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Article Not Found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`No article with id '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' exists.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Versioning
&lt;/h2&gt;

&lt;p&gt;You will change your API. Version it from day one.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Pros&lt;/th&gt;
&lt;th&gt;Cons&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;URL path&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/v1/articles&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Visible, cacheable, loggable, bookmarkable&lt;/td&gt;
&lt;td&gt;URL "leaks" version — semantic purists object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Request header&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;API-Version: 2026-01-01&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clean URLs&lt;/td&gt;
&lt;td&gt;Invisible, hard to test in browser, can't cache differently&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Query param&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/articles?version=1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Easy to test&lt;/td&gt;
&lt;td&gt;Pollutes query space, gets stripped by some proxies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Accept header&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Accept: application/vnd.api.v2+json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;RFC-compliant content negotiation&lt;/td&gt;
&lt;td&gt;Verbose, poor tooling support&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use URL path versioning.&lt;/strong&gt; You'll hear that it violates REST semantics — typically from people who've never had to track down a versioning bug in production logs where the client was silently sending the wrong header. Visibility in logs, cache keys, and browser testability are real, daily benefits. The semantic purity objection is theoretical.&lt;/p&gt;

&lt;h3&gt;
  
  
  State Diagram: API Version Lifecycle
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fku11saojep9w4kizzq9n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fku11saojep9w4kizzq9n.png" alt="State Diagram: API Version Lifecycle" width="464" height="1160"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Announce deprecation in response headers: &lt;code&gt;Deprecation: true&lt;/code&gt;, &lt;code&gt;Sunset: Sat, 01 Jan 2028 00:00:00 GMT&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pagination and Filtering
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Cursor vs Offset
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Criteria&lt;/th&gt;
&lt;th&gt;Offset (&lt;code&gt;?page=2&amp;amp;limit=20&lt;/code&gt;)&lt;/th&gt;
&lt;th&gt;Cursor (&lt;code&gt;?cursor=abc&amp;amp;limit=20&lt;/code&gt;)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Performance at scale&lt;/td&gt;
&lt;td&gt;Degrades — &lt;code&gt;OFFSET 1000000&lt;/code&gt; is slow&lt;/td&gt;
&lt;td&gt;Constant — indexed seek&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stable under concurrent writes&lt;/td&gt;
&lt;td&gt;No — items shift between pages&lt;/td&gt;
&lt;td&gt;Yes — cursor anchors position&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seekable to arbitrary page&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No — must walk forward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Implementation complexity&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Use cursor pagination.&lt;/strong&gt; &lt;code&gt;OFFSET 100000&lt;/code&gt; is a slow table scan dressed up as a query parameter. I've seen this exact pattern take down a read replica during a traffic spike — the query plan looked fine in development on 5,000 rows, then became catastrophic at 2 million under load. Offset pagination is fine for 50-row config lists that never change. For anything that grows or updates frequently, use cursor pagination from day one. Retrofitting it later is painful.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/v1/articles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;20&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cursor&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authorId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author_id&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authorId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;authorId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextCursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;next_cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;nextCursor&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Filtering goes in query parameters. Sorting too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /v1/articles?status=published&amp;amp;author_id=usr_789&amp;amp;sort=-createdAt&amp;amp;limit=20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Convention: prefix &lt;code&gt;-&lt;/code&gt; means descending (&lt;code&gt;-created_at&lt;/code&gt; = newest first).&lt;/p&gt;




&lt;h2&gt;
  
  
  Security Fundamentals
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Authentication
&lt;/h3&gt;

&lt;p&gt;Pass credentials in the &lt;code&gt;Authorization&lt;/code&gt; header. Never in URLs — URLs appear in server logs, browser history, and proxy caches.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Correct&lt;/span&gt;
curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://api.example.com/v1/articles

&lt;span class="c"&gt;# Wrong — token in logs forever&lt;/span&gt;
curl https://api.example.com/v1/articles?token&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$TOKEN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Request Authentication Flow
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0nw7x40vpnnsemkj085j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0nw7x40vpnnsemkj085j.png" alt="Request Authentication Flow" width="800" height="933"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTPS Only
&lt;/h3&gt;

&lt;p&gt;Enforce HTTPS at the gateway level. Reject HTTP with &lt;code&gt;301 Moved Permanently&lt;/code&gt; to the HTTPS equivalent. Never accept credentials over plain HTTP.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rate Limiting Response
&lt;/h3&gt;

&lt;p&gt;When you rate-limit a request, return &lt;code&gt;429 Too Many Requests&lt;/code&gt; with these headers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;429&lt;/span&gt; &lt;span class="ne"&gt;Too Many Requests&lt;/span&gt;
&lt;span class="na"&gt;Retry-After&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;60&lt;/span&gt;
&lt;span class="na"&gt;X-RateLimit-Limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;100&lt;/span&gt;
&lt;span class="na"&gt;X-RateLimit-Remaining&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="na"&gt;X-RateLimit-Reset&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1727780460&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Idempotency Keys
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;POST&lt;/code&gt; is not idempotent — calling it twice creates two resources. Networks are unreliable. A client that retries a timed-out &lt;code&gt;POST /v1/orders&lt;/code&gt; could create a duplicate charge.&lt;/p&gt;

&lt;p&gt;The solution: clients attach a unique &lt;code&gt;Idempotency-Key&lt;/code&gt; header. The server stores the response for that key. If the same key arrives again, return the cached response without re-executing the operation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# First request — creates the article&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.example.com/v1/articles &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"title":"Hello","content":"...","authorId":"usr_1"}'&lt;/span&gt;
&lt;span class="c"&gt;# → 201 Created&lt;/span&gt;

&lt;span class="c"&gt;# Retry with same key (network timeout, client retries)&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://api.example.com/v1/articles &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  ...
&lt;span class="c"&gt;# → 201 Created (same response, no second article created)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Middleware: check and store idempotency keys&lt;/span&gt;
&lt;span class="c1"&gt;// Use Redis with a TTL (e.g., 24 h) in production — Map is in-memory only&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idempotencyStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;withIdempotency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;idempotency-key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;idempotencyStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;originalJson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;idempotencyStore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;originalJson&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Apply to any non-idempotent route&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/v1/articles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;withIdempotency&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ... normal create logic&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Idempotency keys matter most for payment, order, and email-send endpoints. Stripe, PayPal, and Shopify all implement this pattern. Your key store needs a TTL — a key that lives forever leaks memory; 24 hours is the Stripe standard.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1: Verbs in resource names&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;code&gt;POST /v1/sendEmail&lt;/code&gt; treats the action as the resource. The resource is the email — the action is the HTTP method. &lt;code&gt;POST /v1/emails&lt;/code&gt; is correct. If an action genuinely has no resource noun, use a sub-resource: &lt;code&gt;POST /v1/articles/{id}/publish&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2: Returning 200 OK for errors&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Returning &lt;code&gt;{ "success": false, "error": "not found" }&lt;/code&gt; with &lt;code&gt;200 OK&lt;/code&gt; means clients must parse and inspect the body on every response to detect failures. HTTP status codes exist precisely to avoid this. Use them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3: Swallowing 404 vs 403&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Returning &lt;code&gt;404 Not Found&lt;/code&gt; when the resource exists but the caller lacks access is both a correctness bug and a security vulnerability — you're leaking resource existence to unauthorized callers. The correct response is &lt;code&gt;403 Forbidden&lt;/code&gt;. Return &lt;code&gt;404&lt;/code&gt; only when the resource genuinely does not exist (or when you deliberately want to hide its existence — which must be an intentional, documented choice, not an accident).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 4: Offset pagination in high-traffic collections&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;code&gt;SELECT * FROM articles ORDER BY created_at LIMIT 20 OFFSET 10000&lt;/code&gt; requires the database to read and discard 10,000 rows. I've debugged production incidents caused by exactly this — the query looks fine on 5,000 rows in development, then becomes a full table scan in disguise at 2 million rows under peak load. Switch to cursor-based pagination: &lt;code&gt;WHERE id &amp;gt; $cursor ORDER BY id LIMIT 20&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 5: No consistent error shape&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
An API that returns &lt;code&gt;{ "message": "..." }&lt;/code&gt; on some errors, &lt;code&gt;{ "error": { "code": "...", "description": "..." } }&lt;/code&gt; on others, and a plain HTML 500 page on unhandled exceptions forces consumers to write separate error handling for every possible failure mode. Adopt RFC 7807 and return the same shape everywhere — including unhandled 500s.&lt;/p&gt;


&lt;h2&gt;
  
  
  Full Example: Complete Express TypeScript Implementation
&lt;/h2&gt;

&lt;p&gt;Everything covered above in one runnable file. Copy it, run &lt;code&gt;npm run dev&lt;/code&gt;, and use the curl commands below to verify each behavior.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;zod&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;randomUUID&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="c1"&gt;// Types&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Article&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;authorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;published&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Article&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Helpers&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;nowIso&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unknown&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://api.example.com/errors/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/problem+json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Validation schemas&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ArticleCreateSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;authorId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ArticleUpdateSchema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;object&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;z&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;optional&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Routes&lt;/span&gt;

&lt;span class="c1"&gt;// POST /v1/articles — create, 201 + Location header&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/v1/articles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ArticleCreateSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Validation Error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;nowIso&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;article&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;createdAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;updatedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ts&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;setHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Location&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`/v1/articles/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// GET /v1/articles — list with cursor pagination and filtering&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/v1/articles&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;20&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cursor&lt;/span&gt;    &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authorId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;author_id&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;   &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authorId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorId&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;authorId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="nx"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findIndex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextCursor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;pagination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;next_cursor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;nextCursor&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// GET /v1/articles/:id&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/v1/articles/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Article Not Found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`No article with id '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// PATCH /v1/articles/:id — partial update&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/v1/articles/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Article Not Found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`No article with id '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ArticleUpdateSchema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safeParse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;422&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Validation Error&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;   &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;nowIso&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// POST /v1/articles/:id/publish — action as sub-resource&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/v1/articles/:id/publish&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;article&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Article Not Found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`No article with id '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;published&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Already Published&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`Article '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' is already published.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;published&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;nowIso&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;article&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// DELETE /v1/articles/:id — 204 No Content&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/v1/articles/:id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;problem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Article Not Found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`No article with id '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;'.`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;204&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;API running on http://localhost:3000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test the full cycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/v1/articles &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"title":"Hello","content":"World","authorId":"usr_1"}'&lt;/span&gt; | jq

&lt;span class="c"&gt;# List with filter&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:3000/v1/articles?author_id=usr_1&amp;amp;limit=5"&lt;/span&gt; | jq

&lt;span class="c"&gt;# Publish (action as sub-resource)&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/v1/articles/&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;/publish | jq

&lt;span class="c"&gt;# Delete — expect 204&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE http://localhost:3000/v1/articles/&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Full source with tests: &lt;a href="https://github.com/brywritescode/bry-writes-code-examples.git" rel="noopener noreferrer"&gt;GitHub link&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;REST API design is a set of decisions you make once and live with for years. The ones I see cause the most pain aren't exotic — they're the basics: verbs in URLs, inconsistent error shapes, offset pagination on growing datasets, no versioning plan. None of this is new. RFC 7807 has existed since 2016. Cursor pagination predates it by decades.&lt;/p&gt;

&lt;p&gt;The reason these mistakes keep appearing in fresh codebases is that nothing stops you from making them. Frameworks don't enforce it. Tutorials skip it. You have to choose these defaults deliberately.&lt;/p&gt;

&lt;p&gt;Now you have a reason to.&lt;/p&gt;




&lt;h2&gt;
  
  
  Further Reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc7231" rel="noopener noreferrer"&gt;RFC 7231 — HTTP/1.1 Semantics and Content&lt;/a&gt; — authoritative definition of HTTP methods and status codes&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.rfc-editor.org/rfc/rfc7807" rel="noopener noreferrer"&gt;RFC 7807 — Problem Details for HTTP APIs&lt;/a&gt; — structured error response standard&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://cloud.google.com/apis/design" rel="noopener noreferrer"&gt;Google API Design Guide&lt;/a&gt; — resource-oriented design at scale, from a team running production APIs for millions of developers&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If this helped, a like and a follow are appreciated — and if you've solved this differently, drop a comment, I'd like to hear it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Bry Writes Code — cloud and API infrastructure specialist. Building or auditing a REST API? &lt;a href="mailto:brywritescode@gmail.com"&gt;Get in touch&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>typescript</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
