<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ APIs - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ APIs - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Fri, 15 May 2026 22:29:56 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/apis/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Set Up OpenClaw and Design an A2A Plugin Bridge ]]>
                </title>
                <description>
                    <![CDATA[ OpenClaw is getting attention because it turns a popular AI idea into something you can actually run yourself. Instead of opening one more browser tab, you run a Gateway on your own machine or server  ]]>
                </description>
                <link>https://www.freecodecamp.org/news/openclaw-a2a-plugin-architecture-guide/</link>
                <guid isPermaLink="false">69d542ca5da14bc70e7c1559</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Open Source ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ software architecture ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nataraj Sundar ]]>
                </dc:creator>
                <pubDate>Tue, 07 Apr 2026 17:45:46 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/4be03b02-d128-49e9-afcb-fea0f771e746.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>OpenClaw is getting attention because it turns a popular AI idea into something you can actually run yourself. Instead of opening one more browser tab, you run a Gateway on your own machine or server and connect it to communication tools you already use.</p>
<p>That matters because OpenClaw is self-hosted, multi-channel, open source, and built around agent workflows such as sessions, tools, plugins, and multi-agent routing. It feels less like a toy chatbot and more like an operator-controlled agent runtime.</p>
<p>In this guide, you'll do three things. First, you'll learn what OpenClaw is and why developers are paying attention to it. Second, you'll get it running the beginner-friendly way through the dashboard. Third, you'll walk through an original design contribution: a proposed OpenClaw-to-A2A plugin architecture and a <a href="https://github.com/natarajsundar/openclaw-a2a-secure-agent-runtime"><code>proof-of-concept</code></a> relay that shows how OpenClaw’s session model could map to the A2A protocol.</p>
<p>That last part is important, so I want to frame it carefully. The A2A integration in this article is <strong>not</strong> presented as a built-in OpenClaw feature. It's a documented architecture proposal built on top of the extension points OpenClaw already exposes.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>This guide is beginner-friendly for OpenClaw itself, but it assumes a few basics so you can follow the architecture and proof-of-concept sections comfortably.</p>
<p>Before you continue, you should be familiar with:</p>
<ul>
<li><p>Basic JavaScript or Node.js (reading and running scripts)</p>
</li>
<li><p>How HTTP APIs work (requests, responses, JSON payloads)</p>
</li>
<li><p>Using a terminal to run commands</p>
</li>
<li><p>High-level concepts like services, APIs, or microservices</p>
</li>
</ul>
<p>You don't need prior experience with OpenClaw or A2A. The setup steps walk through everything you need to get started.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-what-openclaw-is">What OpenClaw Is</a></p>
</li>
<li><p><a href="#heading-why-openclaw-is-getting-so-much-attention">Why OpenClaw Is Getting So Much Attention</a></p>
</li>
<li><p><a href="#heading-what-the-a2a-protocol-is">What the A2A Protocol Is</a></p>
</li>
<li><p><a href="#heading-how-openclaw-and-a2a-relate">How OpenClaw and A2A Relate</a></p>
</li>
<li><p><a href="#heading-what-you-need-before-you-start">What You Need Before You Start</a></p>
</li>
<li><p><a href="#heading-step-1-install-openclaw">Install OpenClaw</a></p>
</li>
<li><p><a href="#heading-step-2-run-the-onboarding-wizard">Run the Onboarding Wizard</a></p>
</li>
<li><p><a href="#heading-step-3-check-the-gateway-and-open-the-dashboard">Check the Gateway and Open the Dashboard</a></p>
</li>
<li><p><a href="#heading-step-4-use-openclaw-as-a-private-coding-assistant">Use OpenClaw as a Private Coding Assistant</a></p>
</li>
<li><p><a href="#heading-step-5-understand-multi-agent-routing">Understand Multi Agent Routing</a></p>
</li>
<li><p><a href="#heading-where-a2a-could-fit-later">Where A2A Could Fit Later</a></p>
</li>
<li><p><a href="#heading-a-proposed-openclaw-to-a2a-plugin-architecture">A Proposed OpenClaw to A2A Plugin Architecture</a></p>
</li>
<li><p><a href="#heading-build-the-proof-of-concept-relay">Build the Proof of Concept Relay</a></p>
</li>
<li><p><a href="#heading-how-the-proof-of-concept-maps-to-a-real-openclaw-plugin">How the Proof of Concept Maps to a Real OpenClaw Plugin</a></p>
</li>
<li><p><a href="#heading-security-notes-before-you-go-further">Security Notes Before You Go Further</a></p>
</li>
<li><p><a href="#heading-final-thoughts">Final Thoughts</a></p>
</li>
</ol>
<h2 id="heading-what-openclaw-is">What OpenClaw Is</h2>
<p>According to the <a href="https://docs.openclaw.ai/">official docs</a>, OpenClaw is a self-hosted gateway that connects chat apps like WhatsApp, Telegram, Discord, iMessage, and a browser dashboard to AI agents.</p>
<p>That wording is useful because it tells you where OpenClaw sits in the stack. It's not just a model wrapper. It's a Gateway that handles sessions, routing, and app connections, while agents, tools, plugins, and providers do the actual work.</p>
<p>Here is the simplest mental model:</p>
<img src="https://cdn.hashnode.com/uploads/covers/694ca88d5ac09a5d68c63854/ad5f3295-8fdf-4f9c-8488-f69808850295.png" alt="Diagram showing OpenClaw architecture where multiple chat apps and a browser dashboard connect to a central Gateway, which routes requests to different agents that use model providers and tools." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>If you're new to the project, this is the practical way to think about it:</p>
<ul>
<li><p>your chat apps are the front door</p>
</li>
<li><p>the Gateway is the traffic and control layer</p>
</li>
<li><p>the agent is the reasoning layer</p>
</li>
<li><p>the model provider and tools are what let the agent actually do work</p>
</li>
</ul>
<p>That's one reason OpenClaw feels different from a normal browser-only assistant.</p>
<h2 id="heading-why-developers-are-paying-attention-to-openclaw">Why Developers Are Paying Attention to OpenClaw</h2>
<p>OpenClaw is getting a lot of attention for a few reasons.</p>
<p>The first reason is control. The docs position OpenClaw as self-hosted and multi-channel, which means you can run it on your own machine or server instead of depending on a fully hosted assistant.</p>
<p>The second reason is that OpenClaw already looks like an agent platform. The docs talk about sessions, plugins, tools, skills, multi-agent routing, and ACP-backed external coding harnesses. That's a much richer story than “ask a model a question in a web page.”</p>
<p>The third reason is workflow fit. A lot of people don't want another inbox. They want an assistant that can live in the tools they already check every day.</p>
<p>There's also a broader industry trend behind the hype. Developers are actively looking for ways to connect multiple agents and multiple tools without giving up visibility into what's happening. OpenClaw sits directly in that conversation.</p>
<h2 id="heading-what-the-a2a-protocol-is">What the A2A Protocol Is</h2>
<p>A2A, short for Agent2Agent, is an open protocol for communication between agent systems. The <a href="https://a2a-protocol.org/latest/specification/">A2A specification</a> says its purpose is to help independent agent systems discover each other, negotiate interaction modes, manage collaborative tasks, and exchange information without exposing internal memory, tools, or proprietary logic.</p>
<p>That last point matters. A2A is about interoperability between agent systems, not about exposing all of one agent's internals to another.</p>
<p>A2A introduces a few core concepts that are worth learning early:</p>
<ul>
<li><p><strong>Agent Card</strong>: a JSON description of the remote agent, its URL, skills, capabilities, and auth requirements</p>
</li>
<li><p><strong>Task</strong>: the main unit of remote work</p>
</li>
<li><p><strong>Artifact</strong>: the output of a task</p>
</li>
<li><p><strong>Context ID</strong>: a stable interaction boundary across multiple related turns</p>
</li>
</ul>
<p>A2A tasks follow a fairly clean lifecycle:</p>
<img src="https://cdn.hashnode.com/uploads/covers/694ca88d5ac09a5d68c63854/3b5a43e8-dabd-45e3-bff1-0081e2b37e0d.png" alt="State diagram illustrating the A2A task lifecycle including submitted, working, input required, completed, failed, rejected, and canceled states.." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The A2A docs also explain that A2A and MCP are complementary, not competing. A2A is for agent-to-agent collaboration. MCP is for agent-to-tool communication.</p>
<p>That distinction is useful when you compare A2A with OpenClaw, because OpenClaw already has strong local tool and session concepts.</p>
<h2 id="heading-how-openclaw-and-a2a-relate">How OpenClaw and A2A Relate</h2>
<p>OpenClaw and A2A are not the same thing, but they line up in interesting ways.</p>
<p>OpenClaw already documents several features that point in a multi-agent direction:</p>
<ul>
<li><p><a href="https://docs.openclaw.ai/concepts/multi-agent/">multi-agent routing</a> for multiple isolated agents in one running Gateway</p>
</li>
<li><p><a href="https://docs.openclaw.ai/concepts/session-tool/">session tools</a> such as <code>sessions_send</code> and <code>sessions_spawn</code></p>
</li>
<li><p>a <a href="https://docs.openclaw.ai/tools/plugin/">plugin system</a> that can register tools, HTTP routes, Gateway RPC methods, and background services</p>
</li>
<li><p><a href="https://docs.openclaw.ai/tools/acp-agents/">ACP support</a> and the <a href="https://docs.openclaw.ai/cli/acp"><code>openclaw acp</code> bridge</a> for external coding clients</p>
</li>
</ul>
<p>But it's still important to stay precise here.</p>
<p>OpenClaw documents ACP, plugins, and local multi-agent coordination today. The docs I checked do <strong>not</strong> describe native A2A support as a first-class built-in capability.</p>
<p>That means the honest claim is this:</p>
<p><strong>OpenClaw can be meaningfully connected to A2A in theory because the architectural pieces line up, but the A2A bridge still has to be built.</strong></p>
<h3 id="heading-acp-versus-a2a">ACP versus A2A</h3>
<p>ACP and A2A solve different problems.</p>
<p>ACP in OpenClaw today is about bridging an IDE or coding client to a Gateway-backed session.</p>
<p>A2A is about one agent system talking to another agent system across a protocol boundary.</p>
<img src="https://cdn.hashnode.com/uploads/covers/694ca88d5ac09a5d68c63854/9790f239-528c-422f-bbc5-3e82c7f1a171.png" alt="Diagram showing A2A interaction where an OpenClaw agent communicates through a plugin to discover a remote agent via an Agent Card and send tasks for execution." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<img src="https://cdn.hashnode.com/uploads/covers/694ca88d5ac09a5d68c63854/c4d4279b-3099-4c1b-92b6-3eaf817a6e84.png" alt="Diagram showing ACP flow where an IDE or coding client connects through an OpenClaw ACP bridge to a Gateway-backed session." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>That difference is one reason I prefer the phrase <strong>plugin bridge</strong> here instead of <strong>native A2A support</strong>.</p>
<h2 id="heading-what-you-need-before-you-start">What You Need Before You Start</h2>
<p>The easiest first run does <strong>not</strong> require WhatsApp, Telegram, or Discord.</p>
<p>The OpenClaw onboarding docs say the fastest first chat is the dashboard. That makes this a much more approachable beginner setup.</p>
<p>Before you start, you'll need:</p>
<ol>
<li><p>Node 24 if possible, or Node 22.16+ for compatibility</p>
</li>
<li><p>an API key for the model provider you want to use</p>
</li>
<li><p>If you're on Windows, WSL2 is the recommended path for the full experience. Native Windows works for core CLI and Gateway flows, but the docs call out caveats and position WSL2 as the more stable setup.</p>
</li>
<li><p>about five minutes for the first dashboard-based run</p>
</li>
</ol>
<h2 id="heading-step-1-install-openclaw">Step 1: Install OpenClaw</h2>
<p>The official getting-started page recommends the installer script.</p>
<p>On macOS, Linux, or WSL2, run:</p>
<pre><code class="language-bash">curl -fsSL https://openclaw.ai/install.sh | bash
</code></pre>
<p>On Windows PowerShell, the docs show this:</p>
<pre><code class="language-powershell">iwr -useb https://openclaw.ai/install.ps1 | iex
</code></pre>
<p>If you're on Windows, the platform docs recommend installing WSL2 first:</p>
<pre><code class="language-powershell">wsl --install
</code></pre>
<p>Then open Ubuntu and continue with the Linux commands there.</p>
<h2 id="heading-step-2-run-the-onboarding-wizard">Step 2: Run the Onboarding Wizard</h2>
<p>Once the CLI is installed, run the onboarding wizard.</p>
<pre><code class="language-bash">openclaw onboard --install-daemon
</code></pre>
<p>The onboarding wizard is the recommended path in the docs. It configures auth, gateway settings, optional channels, skills, and workspace defaults in one guided flow.</p>
<p>The most beginner-friendly choice is to keep the first run simple. Don't worry about chat apps yet. Get the local Gateway working first.</p>
<h2 id="heading-step-3-check-the-gateway-and-open-the-dashboard">Step 3: Check the Gateway and Open the Dashboard</h2>
<p>After onboarding, verify that the Gateway is running.</p>
<pre><code class="language-bash">openclaw gateway status
</code></pre>
<p>Then open the dashboard:</p>
<pre><code class="language-bash">openclaw dashboard
</code></pre>
<p>The docs call this the fastest first chat because it avoids channel setup. It's also the safest way to start, because the dashboard is local and the OpenClaw docs clearly say the Control UI is an admin surface and should not be exposed publicly.</p>
<p>The beginner setup flow looks like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/694ca88d5ac09a5d68c63854/eab78250-65d6-4d97-be3d-bf7167b9099e.png" alt="Sequence diagram showing OpenClaw setup flow from installation and onboarding to starting the Gateway and opening the dashboard for the first chat." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>If you can chat in the dashboard, your day-zero setup is working.</p>
<h2 id="heading-step-4-use-openclaw-as-a-private-coding-assistant">Step 4: Use OpenClaw as a Private Coding Assistant</h2>
<p>The best first use case is not to drop OpenClaw into a public group chat.</p>
<p>Use it as a private coding assistant in the dashboard.</p>
<p>For example, try a prompt like this:</p>
<blockquote>
<p>I am building a small Node.js utility that reads Markdown files and generates a table of contents. Turn this idea into a project plan, a README outline, and the first five implementation tasks.</p>
</blockquote>
<p>That kind of prompt is ideal for a first run because it gives you something concrete back right away.</p>
<p>You can also use it to:</p>
<ol>
<li><p>turn rough notes into a plan,</p>
</li>
<li><p>summarize a bug report into action items,</p>
</li>
<li><p>draft a README,</p>
</li>
<li><p>propose a folder structure, or</p>
</li>
<li><p>write a safe first implementation checklist.</p>
</li>
</ol>
<p>That is already enough to make OpenClaw useful before you touch any advanced protocol work.</p>
<h2 id="heading-step-5-understand-multi-agent-routing">Step 5: Understand Multi Agent Routing</h2>
<p>Once the basic setup is working, it helps to understand OpenClaw’s local multi-agent model.</p>
<p>The docs describe multi-agent routing as a way to run multiple isolated agents in one Gateway, with separate workspaces, state directories, and sessions.</p>
<p>That means you can imagine setups like this:</p>
<ul>
<li><p>a personal assistant</p>
</li>
<li><p>a coding assistant</p>
</li>
<li><p>a research assistant</p>
</li>
<li><p>an alerts assistant</p>
</li>
</ul>
<p>OpenClaw already has a model for that:</p>
<img src="https://cdn.hashnode.com/uploads/covers/694ca88d5ac09a5d68c63854/c640a7c4-0421-4513-a2c2-658916504e3b.png" alt="Diagram illustrating OpenClaw multi-agent routing where incoming messages are matched to different agents such as main, coding, and alerts, each with separate sessions." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>You don't need to set this up on day one.</p>
<p>But it matters for the A2A discussion, because once you understand how OpenClaw routes work between local agents, it becomes much easier to think about routing work to <strong>remote</strong> agents through a protocol like A2A.</p>
<h2 id="heading-where-a2a-could-fit-later">Where A2A Could Fit Later</h2>
<p>A2A could fit into OpenClaw in two broad ways.</p>
<h3 id="heading-option-1-openclaw-as-an-a2a-client">Option 1: OpenClaw as an A2A Client</h3>
<p>In this model, OpenClaw stays your personal edge assistant.</p>
<p>It receives a request from the dashboard or a chat app, decides the task needs a specialist, discovers a remote A2A agent through an Agent Card, sends the task, waits for updates or artifacts, and translates the result back into a normal OpenClaw reply.</p>
<img src="https://cdn.hashnode.com/uploads/covers/694ca88d5ac09a5d68c63854/99a2e611-54ac-4c0f-8f8f-c1ce3246bb96.png" alt="Diagram showing OpenClaw acting as an A2A client, delegating tasks from a local session to a remote agent via an Agent Card and returning results to the user." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>This is the cleaner story for a personal assistant. OpenClaw stays the front door, and A2A becomes a delegation path behind the scenes.</p>
<h3 id="heading-option-2-openclaw-as-an-a2a-server">Option 2: OpenClaw as an A2A Server</h3>
<p>In this model, OpenClaw exposes some of its own capabilities to other agents.</p>
<p>A plugin could theoretically publish an A2A Agent Card, advertise a narrow skill set, accept A2A tasks, and map those tasks into OpenClaw sessions or sub-agent runs.</p>
<p>That's technically plausible because the plugin system can register HTTP routes, tools, Gateway methods, and background services.</p>
<p>It's also the riskier direction for a personal assistant, which is why I think <strong>client-first</strong> is the right starting point.</p>
<h2 id="heading-a-proposed-openclaw-to-a2a-plugin-architecture">A Proposed OpenClaw to A2A Plugin Architecture</h2>
<p>This section is my original contribution in the article.</p>
<p>I think the cleanest first architecture is <strong>not</strong> a full bidirectional bridge. It's a narrow outbound delegation plugin that lets OpenClaw call a small allowlist of remote A2A agents.</p>
<p>The design goal is simple:</p>
<p><strong>Reuse OpenClaw for user-facing conversations and local tool access, but use A2A only when a remote specialist agent is the best place to do the work.</strong></p>
<p>Here is the architecture I would start with:</p>
<img src="https://cdn.hashnode.com/uploads/covers/694ca88d5ac09a5d68c63854/e88f06dd-f108-48b2-a9ee-b74eac6b733b.png" alt="Architecture diagram of an OpenClaw-to-A2A plugin showing components such as delegation tool, policy engine, Agent Card cache, session-to-task mapper, task poller, and remote A2A agent." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-why-this-design-is-a-good-fit-for-openclaw">Why This Design is a Good Fit for OpenClaw</h3>
<p>This proposal is grounded in extension points OpenClaw already documents.</p>
<p>A plugin can register:</p>
<ul>
<li><p>an <strong>agent tool</strong> for delegation,</p>
</li>
<li><p>a <strong>Gateway method</strong> for health and diagnostics,</p>
</li>
<li><p>an <strong>HTTP route</strong> for future callbacks or webhook verification, and</p>
</li>
<li><p>a <strong>background service</strong> for cache warming, task subscriptions, or cleanup.</p>
</li>
</ul>
<p>That means the bridge doesn't have to modify OpenClaw core to be credible.</p>
<h3 id="heading-the-mapping-table">The Mapping Table</h3>
<p>The most important design decision is how to map OpenClaw’s session model to A2A’s task model.</p>
<p>Here is the mapping I recommend:</p>
<table>
<thead>
<tr>
<th>OpenClaw concept</th>
<th>A2A concept</th>
<th>Why this mapping works</th>
</tr>
</thead>
<tbody><tr>
<td><code>sessionKey</code></td>
<td><code>contextId</code></td>
<td>A single OpenClaw conversation should keep a stable remote context across related delegated turns</td>
</tr>
<tr>
<td>one delegated remote call</td>
<td>one <code>Task</code></td>
<td>each remote specialization request becomes a discrete unit of work</td>
</tr>
<tr>
<td>plugin tool call</td>
<td><code>SendMessage</code></td>
<td>the delegation tool is the natural point where the local agent crosses the protocol boundary</td>
</tr>
<tr>
<td>remote output</td>
<td><code>Artifact</code></td>
<td>A2A wants task outputs returned as artifacts rather than chat-only replies</td>
</tr>
<tr>
<td>plugin HTTP route</td>
<td>callback or future push handler</td>
<td>gives you a place to verify webhooks if you later adopt async push</td>
</tr>
<tr>
<td>Gateway method</td>
<td>status endpoint</td>
<td>gives operators a direct way to inspect relay health without going through the model</td>
</tr>
<tr>
<td>background service</td>
<td>polling or cache work</td>
<td>keeps asynchronous and maintenance work out of the tool call path</td>
</tr>
</tbody></table>
<p>This is the key architectural claim in the article:</p>
<p><strong>Treat the OpenClaw session as the long-lived conversational boundary, and treat each remote A2A task as one delegated execution inside that boundary.</strong></p>
<p>That preserves both sides cleanly.</p>
<h3 id="heading-the-design-in-one-sentence">The Design in One Sentence</h3>
<p>The <code>a2a_delegate</code> tool should:</p>
<ol>
<li><p>resolve an allowlisted remote Agent Card,</p>
</li>
<li><p>reuse an existing A2A <code>contextId</code> for the current <code>sessionKey</code> when possible,</p>
</li>
<li><p>create a fresh remote <code>Task</code> for the new delegated turn,</p>
</li>
<li><p>normalize remote artifacts back into a simple local answer, and</p>
</li>
<li><p>never expose the whole OpenClaw Gateway directly to the public internet.</p>
</li>
</ol>
<p>I like this design because it is incremental, testable, and consistent with OpenClaw’s personal-assistant trust model.</p>
<h2 id="heading-build-the-proof-of-concept-relay">Build the Proof of Concept Relay</h2>
<p>To make the architecture concrete, I built a small proof-of-concept relay.</p>
<p><a href="https://github.com/natarajsundar/openclaw-a2a-secure-agent-runtime">https://github.com/natarajsundar/openclaw-a2a-secure-agent-runtime</a></p>
<p>It's intentionally small. It doesn't try to become a full production plugin. Instead, it proves the hardest conceptual part of the bridge: how to map one OpenClaw session to a reusable A2A context while creating a fresh A2A task per delegated turn.</p>
<p>Here's the repository layout:</p>
<pre><code class="language-plaintext">openclaw-a2a-secure-agent-runtime/
├── README.md
├── package.json
├── examples/
│   └── openclaw-plugin-entry.example.ts
├── src/
│   ├── a2a-client.mjs
│   ├── agent-card-cache.mjs
│   ├── demo.mjs
│   ├── mock-remote-agent.mjs
│   ├── openclaw-a2a-relay.mjs
│   ├── session-task-map.mjs
│   └── utils.mjs
└── test/
    └── relay.test.mjs
</code></pre>
<p>The PoC does six things:</p>
<ol>
<li><p>fetches a remote Agent Card from <code>/.well-known/agent-card.json</code>,</p>
</li>
<li><p>caches it with simple <code>ETag</code> revalidation,</p>
</li>
<li><p>records local <code>sessionKey</code> to remote <code>contextId</code> mappings,</p>
</li>
<li><p>sends an A2A <code>SendMessage</code> request,</p>
</li>
<li><p>polls <code>GetTask</code> until the task finishes, and</p>
</li>
<li><p>converts the remote artifact into a local text answer.</p>
</li>
</ol>
<h3 id="heading-run-the-demo">Run the Demo</h3>
<p>The repo uses only built-in Node.js modules.</p>
<pre><code class="language-shell">cd openclaw-a2a-secure-agent-runtime
npm run demo
</code></pre>
<p>The demo spins up a mock remote A2A server, delegates one task, delegates a second task from the <strong>same</strong> local session, and shows that the same remote <code>contextId</code> is reused.</p>
<h3 id="heading-the-core-relay-idea">The Core Relay Idea</h3>
<p>This is the important logic in plain English:</p>
<ol>
<li><p>look up the most recent remote mapping for the current OpenClaw <code>sessionKey</code></p>
</li>
<li><p>reuse the old <code>contextId</code> if one exists</p>
</li>
<li><p>create a fresh A2A <code>Task</code> for the new request</p>
</li>
<li><p>poll until that task becomes <code>TASK_STATE_COMPLETED</code></p>
</li>
<li><p>turn the returned artifact into a normal text result that OpenClaw can send back to the user</p>
</li>
</ol>
<p>That makes the bridge predictable.</p>
<p>Here's a shortened version of the relay logic:</p>
<pre><code class="language-js">const previous = await sessionTaskMap.latestForSession(sessionKey, remoteBaseUrl);
const contextId = previous?.contextId ?? crypto.randomUUID();

const sendResult = await client.sendMessage({
  text,
  contextId,
  metadata: {
    openclawSessionKey: sessionKey,
    requestedSkillId: skillId,
  },
});

let task = sendResult.task;
while (!isTerminalTaskState(task.status?.state)) {
  await sleep(pollIntervalMs);
  task = await client.getTask(task.id);
}

return {
  contextId,
  taskId: task.id,
  answer: taskArtifactsToText(task),
};
</code></pre>
<p>That's the heart of the design.</p>
<h3 id="heading-why-this-repo-is-a-useful-proof-of-concept">Why This Repo is a Useful Proof of Concept</h3>
<p>A lot of “integration” articles stay too abstract. This repo avoids that problem in three ways.</p>
<p>First, it makes the session-to-context mapping explicit.</p>
<p>Second, it includes a mock remote A2A agent so you can test the flow without needing a large external setup.</p>
<p>Third, it includes a test that checks the most important invariant: repeated delegations from one local OpenClaw session reuse the same A2A context.</p>
<p>That is the piece I most wanted to make concrete, because it is where architecture turns into implementation.</p>
<h2 id="heading-how-the-proof-of-concept-maps-to-a-real-openclaw-plugin">How the Proof of Concept Maps to a Real OpenClaw Plugin</h2>
<p>The proof of concept is the relay core.</p>
<p>A real OpenClaw plugin would wrap that relay with four extension surfaces that the OpenClaw docs already describe.</p>
<h3 id="heading-1-a-delegation-tool">1: A Delegation Tool</h3>
<p>This is the main entry point.</p>
<p>A plugin would register an optional tool like <code>a2a_delegate</code> so the local agent can explicitly choose to delegate work.</p>
<p>That tool should be optional, not always-on, because remote delegation is a side effect and should be easy to disable.</p>
<h3 id="heading-2-a-gateway-method-for-diagnostics">2: A Gateway Method for Diagnostics</h3>
<p>A method like <code>a2a.status</code> would let you inspect whether the relay is healthy, which remote cards are cached, and whether any tasks are still being tracked.</p>
<p>That is much better than asking the model to “tell me if the bridge is healthy.”</p>
<h3 id="heading-3-a-plugin-http-route">3: A Plugin HTTP Route</h3>
<p>You may not need this on day one.</p>
<p>But once you move beyond polling and want push-style callbacks or webhook verification, a plugin route gives you the right boundary for that work.</p>
<h3 id="heading-4-a-background-service">4: A Background Service</h3>
<p>A small service is a clean place to do cache warming, cleanup, or later subscription handling.</p>
<p>That keeps the tool path focused on delegation instead of maintenance work.</p>
<p>If I were turning this into a real plugin package, I would sequence the work in this order:</p>
<ol>
<li><p>wrap the relay in <code>registerTool</code>,</p>
</li>
<li><p>add a small config schema with an allowlist of remote agents,</p>
</li>
<li><p>add <code>a2a.status</code>,</p>
</li>
<li><p>keep polling as the first async model,</p>
</li>
<li><p>add a callback route only if a real use case needs it.</p>
</li>
</ol>
<p>That is the most practical path from theory to a real extension.</p>
<p>I tested the relay flow locally with the mock remote agent and confirmed that repeated delegations from the same local session reused the same remote <code>contextId</code>.</p>
<h2 id="heading-security-notes-before-you-go-further">Security Notes Before You Go Further</h2>
<p>This is the section you should not skip.</p>
<p>The OpenClaw security docs explicitly say the project assumes a <strong>personal assistant</strong> trust model: one trusted operator boundary per Gateway. They also say a shared Gateway for mutually untrusted or adversarial users is not the supported boundary model.</p>
<p>That has a direct consequence for A2A.</p>
<p>A2A is designed for communication across agent systems and organizational boundaries. That is powerful, but it is also a different threat model from a single private OpenClaw deployment.</p>
<p>So the safer design is <strong>not</strong> this:</p>
<ul>
<li><p>expose your personal OpenClaw Gateway publicly,</p>
</li>
<li><p>let arbitrary remote agents reach it,</p>
</li>
<li><p>and hope the tool boundaries are enough.</p>
</li>
</ul>
<p>The safer design is closer to this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/694ca88d5ac09a5d68c63854/5ab4460a-6c00-4880-a29c-ddc1db00b5fa.png" alt="Diagram illustrating separation between a private OpenClaw deployment and an external A2A interoperability boundary, highlighting secure delegation through a controlled relay." style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>This diagram shows two separate trust boundaries.</p>
<p>On the left is your <strong>private OpenClaw deployment</strong>. This includes your Gateway, your sessions, your workspace, and any credentials or tools your agent can access. This boundary is designed for a single trusted operator.</p>
<p>On the right is the <strong>external A2A ecosystem</strong>, where remote agents live. These agents may belong to other teams or organizations and operate under different security assumptions.</p>
<p>The key idea is that communication between these two sides should happen through a <strong>controlled relay layer</strong>, not by directly exposing your OpenClaw Gateway. The relay enforces allowlists, limits what data is sent out, and ensures that remote agents cannot directly access your local tools or state.</p>
<p>This separation lets you experiment with agent interoperability while keeping your personal assistant environment safe.</p>
<p>In plain English, keep your personal assistant boundary private.</p>
<p>If you experiment with A2A, treat that as a <strong>separate exposure boundary</strong> with its own allowlists, auth, and operational controls.</p>
<p>That is why the proof-of-concept relay in this article starts with an explicit remote allowlist.</p>
<h3 id="heading-why-this-design-and-not-the-other-one">Why This Design and Not the Other One?</h3>
<p>A natural question is why this article proposes an <strong>outbound-only A2A bridge first</strong>, instead of immediately building a full bidirectional or server-style integration.</p>
<p>The short answer is that OpenClaw’s current design is centered around a <strong>personal assistant trust boundary</strong>, where one operator controls the Gateway, sessions, and tools. Introducing external agents into that environment requires careful control over what is exposed.</p>
<p>Starting with outbound delegation gives you a safer and more incremental path.</p>
<p>Outbound-only first means:</p>
<ul>
<li><p>preserving the personal-assistant trust boundary, so your local OpenClaw deployment remains private and operator-controlled</p>
</li>
<li><p>avoiding exposing the OpenClaw Gateway as a public A2A server before you have strong auth, policy, and monitoring in place</p>
</li>
<li><p>allowing you to test remote delegation patterns (Agent Cards, tasks, artifacts) without committing to full interoperability complexity</p>
</li>
<li><p>keeping OpenClaw as the user-facing control plane, while remote agents act as optional specialists</p>
</li>
</ul>
<p>This approach follows a common systems design pattern: start with <strong>controlled outbound integration</strong>, validate behavior and constraints, and only then consider expanding to inbound or bidirectional communication.</p>
<p>In practice, this means you can experiment with A2A safely, learn how the models fit together, and evolve the system without introducing unnecessary risk early on.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>OpenClaw is worth learning because it gives you a self-hosted assistant that can live in the communication tools you already use.</p>
<p>The simplest beginner path is still the right one:</p>
<ol>
<li><p>install it,</p>
</li>
<li><p>run onboarding,</p>
</li>
<li><p>check the Gateway,</p>
</li>
<li><p>open the dashboard,</p>
</li>
<li><p>try one private workflow.</p>
</li>
</ol>
<p>That is already a real end-to-end setup.</p>
<p>A2A belongs in the conversation because it gives you a credible way to connect OpenClaw to remote specialist agents later.</p>
<p>But the most important thing in this article isn't the buzzword. It's the boundary design.</p>
<p>If you keep OpenClaw as the private user-facing edge and use a narrow plugin bridge for outbound delegation, the OpenClaw session model and the A2A task model can fit together cleanly.</p>
<p>That is the architectural idea I wanted to make concrete here.</p>
<h3 id="heading-diagram-attribution">Diagram Attribution</h3>
<p>All diagrams in this article were created by the author specifically for this guide.</p>
<h2 id="heading-further-reading">Further Reading</h2>
<ul>
<li><p><a href="https://docs.openclaw.ai/">OpenClaw docs home</a></p>
</li>
<li><p><a href="https://docs.openclaw.ai/start/getting-started">OpenClaw Getting Started</a></p>
</li>
<li><p><a href="https://docs.openclaw.ai/start/wizard">OpenClaw Onboarding Wizard</a></p>
</li>
<li><p><a href="https://docs.openclaw.ai/concepts/multi-agent/">OpenClaw Multi-Agent Routing</a></p>
</li>
<li><p><a href="https://docs.openclaw.ai/concepts/session-tool/">OpenClaw Session Tools</a></p>
</li>
<li><p><a href="https://docs.openclaw.ai/tools/plugin/">OpenClaw Plugin System</a></p>
</li>
<li><p><a href="https://docs.openclaw.ai/plugins/agent-tools">OpenClaw Plugin Agent Tools</a></p>
</li>
<li><p><a href="https://docs.openclaw.ai/cli/acp">OpenClaw ACP bridge</a></p>
</li>
<li><p><a href="https://docs.openclaw.ai/gateway/security">OpenClaw Security</a></p>
</li>
<li><p><a href="https://a2a-protocol.org/latest/specification/">A2A specification</a></p>
</li>
<li><p><a href="https://a2a-protocol.org/latest/topics/agent-discovery/">A2A Agent Discovery</a></p>
</li>
<li><p><a href="https://a2a-protocol.org/latest/topics/a2a-and-mcp/">A2A and MCP</a></p>
</li>
<li><p><a href="https://a2a-protocol.org/latest/definitions/">A2A protocol definition and schema</a></p>
</li>
<li><p><a href="https://a2a-protocol.org/latest/announcing-1.0/">A2A version 1.0 announcement</a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Override API Responses and Headers in Chrome DevTools: A Step-by-Step Guide ]]>
                </title>
                <description>
                    <![CDATA[ Have you ever faced a situation as a frontend developer where you needed to show a demo to your product manager, and something was broken in the API response? Or, a production bug where you were block ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-override-api-responses-and-headers-in-chrome-devtools/</link>
                <guid isPermaLink="false">69c5a33110e664c5da34c6d3</guid>
                
                    <category>
                        <![CDATA[ Frontend Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Productivity ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tapas Adhikary ]]>
                </dc:creator>
                <pubDate>Thu, 26 Mar 2026 21:20:49 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/a2fee8e9-cb71-4065-af8e-ef0357e6ca2c.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Have you ever faced a situation as a frontend developer where you needed to show a demo to your product manager, and something was broken in the API response? Or, a production bug where you were blocked by waiting on your backend team to provide you with a fix to implement further on the frontend?</p>
<p>To make things even worse, how about a CORS error that completely prevented you from showing the page?</p>
<p>It happens, right? Going back to the backend team and getting a quick fix for these issues would be ideal, but may not be realistic in most cases. It depends on the availability of the backend developers, the priority items they're working on, communication protocols between teams, and even interpersonal relationships.</p>
<p>Now, the question is, can you afford to wait? Wait for things to work in your favour within the given deadline so that you can show that demo or deliver the work to your customer? The answer is NO. You may not have that luxury.</p>
<p>Today, in this article, you'll learn a couple of mind-blowing tips and tricks that will save you from these situations. You'll understand how to set up your Chrome browser so that you can continue with your frontend development even when the backend API returns an incorrect response or a CORS error.</p>
<p>This is a step-by-step guide that'll help make you comfortable with all the required configurations and get you set up to use it on your web projects. This guide is also available as a video tutorial as part of the <a href="https://www.youtube.com/playlist?list=PLIJrr73KDmRwT8Msc4H3_CP5Tf8MqqqVZ">Thinking in Debugging</a> series. You can check it out if you’d like:</p>
<div class="embed-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/ndrPcDNmwFk" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>

<p>Let's get started with the problems and their solutions.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-problem-1-backend-response-is-wrong">Problem 1: The Backend Response Is Wrong</a></p>
</li>
<li><p><a href="#heading-problem-2-validating-a-ui-scenario-without-backend-changes">Problem 2: Validating a UI Scenario Without Backend Changes</a></p>
</li>
<li><p><a href="#heading-problem-3-handling-cors-errors">Problem 3: Handling CORS Errors</a></p>
</li>
<li><p><a href="#heading-additional-tips">Additional Tips</a></p>
<ul>
<li><p><a href="#heading-applying-overrides-globally">Applying Overrides Globally</a></p>
</li>
<li><p><a href="#heading-disabling-or-removing-overrides">Disabling or Removing Overrides</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-learn-more-from-the-thinking-in-debugging-mindset">Learn More From the Thinking in Debugging Mindset</a></p>
</li>
<li><p><a href="#heading-before-we-end">Before We End…</a></p>
</li>
</ol>
<h2 id="heading-problem-1-the-backend-response-is-wrong">Problem 1: The Backend Response Is Wrong</h2>
<p>Here is a classic case of a wrong API response, but you still need to continue with your frontend work.</p>
<p>Take a look at the image below. Do you see that something is off? Yeah, on the first card, the spelling of <code>Banana</code> seems to be misspelled as <code>Banananana</code>. This user interface is constructed using the data we have received as an API response.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/2bef7ac2-98c0-419a-b783-ee64900f8121.png" alt="Incorrect spelling of banana on first card" style="display:block;margin:0 auto" width="2076" height="1546" loading="lazy">

<p>We can go to the backend team and request that they fix it as soon as possible. But it may not happen until the next sprint starts, which might be 15 days from now.</p>
<p>So, what can we do to continue with our work and all the validations on the frontend side? We can use the <code>Content Overriding</code> feature of the Chrome browser to mitigate this situation.</p>
<h3 id="heading-how-to-use-content-overriding">How to Use <code>Content Overriding</code></h3>
<p>First, open up the DevTools of your Chrome browser by pressing the F12 (on Mac, Cmd+F12) key. Then move to the network tab and inspect the API request that returns the incorrect response.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/f9689133-c7cd-400e-b0cc-5275fe1ca7ec.png" alt="Network Request" style="display:block;margin:0 auto" width="1870" height="808" loading="lazy">

<p>Next, right-click on the API request and select the <code>Override content</code> option from the context menu.</p>
<p>You may wonder what content means here, and what I am overriding? You're overriding the API response so that it can reflect on the UI locally.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/31d4e340-d9fa-4dec-81cc-08fbf644aa58.png" alt="Override Content Option" style="display:block;margin:0 auto" width="814" height="1090" loading="lazy">

<p>This will bring up a UI at the top where you can select a folder to store override files. It's important to understand that all the content overrides are locally stored on your machine's hard disk. This means you can use these persisted overrides again and again until someone fixes the issue permanently at the backend.</p>
<p>Now click on the <code>Select folder</code> button.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/7d238b9d-146c-4592-9008-26c7c189a0de.png" alt="Select a Folder" style="display:block;margin:0 auto" width="1890" height="1530" loading="lazy">

<p>This will open up the folder explorer for you. Create a new folder and select it, or select an existing folder where you want to save the overrides. In my case, I've named the folder as <code>debug_devtools</code>.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/7bc13c7b-0ffa-442a-899e-98a805fcddac.png" alt="Select a Folder" style="display:block;margin:0 auto" width="1564" height="1062" loading="lazy">

<p>Now, Chrome DevTools will ask for confirmation that you're allowing DevTools to edit files on your local system. Just click on the <code>Allow</code> button.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/07ff30f6-8bef-40c3-aee9-bcd55545c67f.png" alt="Allow" style="display:block;margin:0 auto" width="1504" height="682" loading="lazy">

<p>That's all for the setup. Now, you'll find the same response in the editable mode under the <code>Sources</code> tab of DevTools. Let's take a deeper look at the image below:</p>
<ol>
<li><p>The Local overrides are listed under the <code>Sources &gt; Oveerides</code> tab of the DevTools.</p>
</li>
<li><p>On the left side, the <code>Enable Local Overrides</code> checkbox is selected, and the overrides are listed below. You can find the same folder you created before, and under that, you'll see another folder called <code>localhost:3001</code> and a file called <code>edibles</code> under it. The localhost:3001 folder name is related to the API endpoint namespace we're connecting to. The edibles file name under it goes with the request name.</p>
</li>
<li><p>On the right side, you can see the content (that is, the response to the edibles request) in editable mode.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/fc918250-c1ab-4b56-8505-3032e698b8cc.png" alt="fc918250-c1ab-4b56-8505-3032e698b8cc" style="display:block;margin:0 auto" width="1818" height="1410" loading="lazy">

<p>You can even cross-check now by traversing to the file system's <code>debug_devtools</code> folder. You should find the same folders and files as you saw in the DevTools.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/81fd4e82-ae7d-4d07-88d8-5a757edbc3ef.png" alt="Response in folder" style="display:block;margin:0 auto" width="1460" height="662" loading="lazy">

<p>You can open up the <code>edibles</code> file. The file content should match exactly the response you saw before.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/4ba75464-0a29-4f11-9bc6-be0463e10dd5.png" alt="edibles content" style="display:block;margin:0 auto" width="1686" height="1148" loading="lazy">

<p>Now, it's time to override. Coming to the Sources tab's editable response panel, you can fix the spelling. Save your changes using Ctrl + S (or Cmd + S).</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/c33701f0-ddb1-478c-8fe6-d68ddf8dd638.png" alt="edit text" style="display:block;margin:0 auto" width="1634" height="700" loading="lazy">

<p>Now, hard refresh your browser. You should be able to see your change reflected on the UI.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/1414be9d-adf4-41a6-8cbc-d115a49bb8a6.png" alt="Banana Fixed" style="display:block;margin:0 auto" width="2982" height="1590" loading="lazy">

<p>Awesome!! You can now share this overridden response (the <code>edibles</code> file) with other developers to point to from their Chrome DevTools to get the same local fix until the backend fixes it.</p>
<h2 id="heading-problem-2-validating-a-ui-scenario-without-backend-changes">Problem 2: Validating a UI Scenario Without Backend Changes</h2>
<p>Imagine you need to validate that certain items are low in stock on an item listing page. If the stock quantity of an item hits 50 or below, you want to show a <code>Low Stock</code> label for that item.</p>
<p>Now, what if the API response doesn't return a quantity of 50 or below? Content overriding can come to the rescue once again!</p>
<p>You can edit the response to set the quantity value to 50 or below and follow the same process as before to reflect the change on the UI. Look at the image below:</p>
<ol>
<li><p>We have edited the quantity on the right-side panel.</p>
</li>
<li><p>Once saved and refreshed, we not only see the updated count on the UI, but it also runs the underlying JavaScript logic to show the <code>Low Stock</code> label automatically. This is a superpower.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/77b8578e-ecc9-4397-b980-e24cc0ab099e.png" alt="Stock" style="display:block;margin:0 auto" width="2660" height="684" loading="lazy">

<h2 id="heading-problem-3-handling-cors-errors">Problem 3: Handling CORS Errors</h2>
<p><a href="https://www.youtube.com/shorts/lPiQClBVYY4">Cross-Origin Resource Sharing (CORS)</a> is a browser security feature that allows a web server to explicitly grant requests coming from a domain other than its own. By default, browsers don't allow these cross-origin requests and follow a strict rule called <code>Same Origin Resource Sharing</code>.</p>
<p>In many cases, your API server and the web server could be hosted on different domains. In those cases, when the web application attempts to access an API, it faces the CORS error.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/f739eb08-6bb1-4534-9080-0417d511396c.png" alt="CORS Error" style="display:block;margin:0 auto" width="2982" height="742" loading="lazy">

<p>On the server side, you need to have explicit configurations to allow cross-origin requests. For example, you need to add the following response headers:</p>
<pre><code class="language-shell">Access-Control-Allow-Origin: http://localhost:5174
Access-Control-Allow-Methods: GET
Access-Control-Allow-Headers: *
</code></pre>
<p>So, again, it may not be guaranteed that your CORS error will be resolved at the server side as soon as you want. But you cann't afford to get blocked due to it. So, what's the way around? Yes! The overriding, but this time, overriding the response header.</p>
<p>Go to the network tab of the Chrome DevTools and right-click on the request that has the CORS error. Now, select the <code>Override headers</code> option from the context menu.</p>
<p>By the way, have you noticed that the <code>Override content</code> option is disabled here? This is because we don't have any response as content from this request, as it got an error.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/4d81fbb1-e960-48f2-bdd7-597e686faff6.png" alt="4d81fbb1-e960-48f2-bdd7-597e686faff6" style="display:block;margin:0 auto" width="1280" height="966" loading="lazy">

<p>Clicking on the <code>Overriding headers</code> will take you to the <code>Headers</code> tab where you can find the option to add additional headers to the response headers. Click on the <code>+ Add header</code> button to add the CORS-related headers.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/9acdbde3-0b12-4cef-9437-844ca92d502c.png" alt="Add Header" style="display:block;margin:0 auto" width="1736" height="1036" loading="lazy">

<p>Add all three headers with their respective values one by one:</p>
<pre><code class="language-bash">Access-Control-Allow-Origin: http://localhost:5174 
Access-Control-Allow-Methods: GET 
Access-Control-Allow-Headers: *
</code></pre>
<p>Each of these headers has its own important use:</p>
<ol>
<li><p>With the <code>Access-Control-Allow-Origin</code> header, you can specify the origin domains that are allowed to have a cross-origin request to the server. In this case, the value is <code>http://localhost:5174</code> where we're running a Vite-based ReactJS app.</p>
</li>
<li><p>The header <code>Access-Control-Allow-Methods</code> specifies what kind of HTTP methods are allowed from the originating domain. In this case, we're allowing only the <code>GET</code> method.</p>
</li>
<li><p>The <code>Access-Control-Allow-Headers</code> HTTP response headers specify which HTTP headers can be safely used during a cross-origin request.</p>
</li>
</ol>
<p>Alright, let's add them all and save.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/02fc7203-9ca6-47cf-a478-f20d928ece54.png" alt="CORS Headers" style="display:block;margin:0 auto" width="1714" height="690" loading="lazy">

<p>Like overriding content, overriding the header will also create a folder with the context of the server domain, and under that, a file called <code>.headers</code>. As the file name starts with a dot(.), it may be treated as a hidden file by most operating systems. So make sure you go through the OS settings to view the hidden files to view this file.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/ab93a8d4-b87d-4cf4-8a0e-1d6ce4d26537.png" alt="Hidden headers file" style="display:block;margin:0 auto" width="1394" height="530" loading="lazy">

<p>Once you view and open the file, you'll see the headers you have added with overriding.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/8dfe3ee3-2250-489a-a465-f9796934a470.png" alt="headers content" style="display:block;margin:0 auto" width="1678" height="1122" loading="lazy">

<p>Now, hard refresh your browser, and try to perform the same operation that was giving you the CORS error before. Wow, the error has gone now! You should be able to see the request success and the response coming back from the server.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/351dcb19-af06-4058-b08d-d449b92ab898.png" alt="User Data" style="display:block;margin:0 auto" width="2558" height="1080" loading="lazy">

<p>Just imagine, not a single line of server-side code changes, and you're unblocked so you can move forward with your client-side UI work. Fantastic, isn't it?</p>
<h2 id="heading-additional-tips">Additional Tips</h2>
<p>Before we end, let's learn about a couple more handy tips.</p>
<h3 id="heading-applying-overrides-globally">Applying Overrides Globally</h3>
<p>We applied the CORS error-related header overriding only on the <code>/user</code> API endpoint. What if you need to apply the same overriding for other endpoints, too? You can do it easily by following these simple steps:</p>
<ol>
<li><p>Navigate to the <code>Sources</code> tab.</p>
</li>
<li><p>Select the <code>Overrides</code> sub-tab.</p>
</li>
<li><p>Click on the <code>.headers</code> override.</p>
</li>
<li><p>On the right-side panel, change the value of the <code>Apply to</code> to <code>*</code>.</p>
</li>
</ol>
<p>That's it. Now, the same response headers will be applied as overrides for all the endpoints.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/c15a94e4-a2e9-48b1-8ae1-c781424be388.png" alt="Apply To" style="display:block;margin:0 auto" width="1678" height="666" loading="lazy">

<h3 id="heading-disabling-or-removing-overrides">Disabling or Removing Overrides</h3>
<p>Sometimes, you might want to disable or remove overrides. To disable overrides without removing them, just uncheck the <code>Enable Local Overrides</code> checkbox. To remove all the overrides permanently, click on the stop icon at the top-right corner. Also, to selectively remove an override, right-click on it and delete.</p>
<img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/80c59dc2-0eea-48ad-b499-be1d4493512c.png" alt="80c59dc2-0eea-48ad-b499-be1d4493512c" style="display:block;margin:0 auto" width="848" height="642" loading="lazy">

<h2 id="heading-learn-more-from-the-thinking-in-debugging-mindset">Learn More From the Thinking in Debugging Mindset</h2>
<p>If you've liked this practical, example-driven guide, you'll enjoy my other debugging-related content from the <em>Thinking in Debugging</em> series. Please <a href="https://www.youtube.com/playlist?list=PLIJrr73KDmRwT8Msc4H3_CP5Tf8MqqqVZ">check it out</a>.</p>
<p><a href="https://www.youtube.com/playlist?list=PLIJrr73KDmRwT8Msc4H3_CP5Tf8MqqqVZ"><img src="https://cdn.hashnode.com/uploads/covers/5c9bb4026656f09759cdc1f0/9046f04f-71f1-4303-b39c-7267fd3814bc.png" alt="Thinking in Debugging" style="display:block;margin:0 auto" width="2330" height="1046" loading="lazy"></a></p>
<h2 id="heading-before-we-end">Before We End…</h2>
<p>That’s all! I hope you found this insightful.</p>
<p>Let’s connect:</p>
<ul>
<li><p>Subscribe to my <a href="https://www.youtube.com/tapasadhikary?sub_confirmation=1">YouTube Channel</a>.</p>
</li>
<li><p>Check out my courses, <a href="https://www.youtube.com/playlist?list=PLIJrr73KDmRw2Fwwjt6cPC_tk5vcSICCu">40 Days of JavaScript</a> and <a href="https://www.youtube.com/playlist?list=PLIJrr73KDmRyQVT__uFZvaVfWPdfyMFHC">15 Days of React Design Patterns</a></p>
</li>
<li><p>Follow on <a href="https://www.linkedin.com/in/tapasadhikary/">LinkedIn</a> if you don't want to miss the daily dose of up-skilling tips.</p>
</li>
<li><p>Join my <a href="https://discord.gg/zHHXx4vc2H">Discord Server</a>, and let’s learn together.</p>
</li>
<li><p>Follow my work on <a href="https://github.com/tapascript">GitHub</a>.</p>
</li>
</ul>
<p>See you soon with my next article. Until then, please take care of yourself and keep learning.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Infrastructure as Code with APIs: How to Automate Cloud Resources the Developer Way ]]>
                </title>
                <description>
                    <![CDATA[ Modern software development moves fast. Teams deploy code many times a day. New environments appear and disappear constantly. In this world, manual infrastructure setup simply doesn't scale. For years ]]>
                </description>
                <link>https://www.freecodecamp.org/news/iac-with-apis-how-to-automate-cloud-resources/</link>
                <guid isPermaLink="false">69c17f3c30a9b81e3a894704</guid>
                
                    <category>
                        <![CDATA[ #IaC ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Cloud Computing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Mon, 23 Mar 2026 17:58:20 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/d45b828b-c7b7-4138-a373-6edea786bc65.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Modern software development moves fast. Teams deploy code many times a day. New environments appear and disappear constantly. In this world, manual infrastructure setup simply doesn't scale.</p>
<p>For years, developers logged into dashboards, clicked through forms, and configured servers by hand. This worked for small projects, but it quickly became fragile. Every manual step increased the chance of mistakes. Environments drifted apart. Reproducing the same setup became difficult.</p>
<p><a href="https://www.redhat.com/en/topics/automation/what-is-infrastructure-as-code-iac">Infrastructure as Code (IaC)</a> solves this problem. Instead of clicking through interfaces, developers define infrastructure using code. This approach makes infrastructure predictable, repeatable, and easy to automate.</p>
<p>In recent years, another approach has become popular alongside traditional IaC tools: using cloud APIs directly to create and manage infrastructure. This gives developers full control over how resources are provisioned and integrated into workflows.</p>
<p>This article explains what Infrastructure as Code means, why APIs are a powerful way to implement it, and how developers can automate cloud resources using simple scripts.</p>
<p>A basic understanding of cloud platforms, command-line interfaces, and scripting languages like Python, Bash, or JavaScript will help you follow along effectively. Familiarity with APIs, authentication methods, and CI/CD concepts will also make it easier to implement the automation techniques discussed in this article.</p>
<h2 id="heading-heres-what-well-cover">Here's what we'll cover:</h2>
<ul>
<li><p><a href="#heading-what-is-infrastructure-as-code">What Is Infrastructure as Code?</a></p>
</li>
<li><p><a href="#heading-the-limits-of-manual-infrastructure">The Limits of Manual Infrastructure</a></p>
</li>
<li><p><a href="#heading-why-apis-are-a-powerful-iac-tool">Why APIs Are a Powerful IaC Tool</a></p>
</li>
<li><p><a href="#heading-automating-infrastructure-with-scripts">Automating Infrastructure with Scripts</a></p>
</li>
<li><p><a href="#heading-practical-example-with-sevalla">Practical Example with Sevalla</a></p>
<ul>
<li><p><a href="#heading-installing-cli">Installing CLI</a></p>
</li>
<li><p><a href="#heading-working-with-your-infrastructure-using-cli">Working with your Infrastructure using CLI</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-infrastructure-as-code-improves-developer-productivity">Infrastructure as Code Improves Developer Productivity</a></p>
</li>
<li><p><a href="#heading-the-future-of-infrastructure">The Future of Infrastructure</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-infrastructure-as-code">What Is Infrastructure as&nbsp;Code?</h2>
<p>Infrastructure as Code means managing infrastructure using code instead of manual processes.</p>
<p>Instead of setting up servers, databases, and networks by hand, you define them in scripts or configuration files. These files describe the desired state of your infrastructure. A tool or script then creates and maintains that state automatically.</p>
<p>For example, instead of manually creating a database, you might define it in code like this:</p>
<pre><code class="language-plaintext">database:
  name: app_db
  engine: postgres
  version: 16
</code></pre>
<p>Once the code runs, the database is created automatically.</p>
<p>This approach provides several key benefits.</p>
<p>First, it improves consistency. Every environment is created from the same definition. Development, staging, and production environments stay aligned.</p>
<p>Second, it improves repeatability. If infrastructure fails, it can be recreated from code in minutes.</p>
<p>Third, it improves version control. Infrastructure definitions live in the same repositories as application code. Teams can review, track, and roll back changes.</p>
<p>Finally, it enables automation. Infrastructure can be created during deployments, tests, or CI/CD pipelines.</p>
<h2 id="heading-the-limits-of-manual-infrastructure">The Limits of Manual Infrastructure</h2>
<p>Before IaC became common, infrastructure management relied heavily on dashboards and manual configuration.</p>
<p>A developer would open a cloud console and perform steps like:</p>
<ul>
<li><p>Create a server</p>
</li>
<li><p>Attach storage</p>
</li>
<li><p>Configure environment variables</p>
</li>
<li><p>Connect a database</p>
</li>
<li><p>Add a domain</p>
</li>
</ul>
<p>These steps worked, but they introduced problems.</p>
<p>First of all, manual configuration is hard to document. Even if teams write guides, small details are often missed. Over time, environments drift apart.</p>
<p>Manual processes also slow down development. Spinning up a new environment may take hours instead of seconds.</p>
<p>Even worse, manual infrastructure cannot easily be tested. If something breaks, reproducing the same conditions becomes difficult.</p>
<p>Infrastructure as Code removes these problems by turning infrastructure into something that can be scripted, tested, and automated.</p>
<h2 id="heading-why-apis-are-a-powerful-iac-tool">Why APIs Are a Powerful IaC&nbsp;Tool</h2>
<p>Many people associate Infrastructure as Code with tools like Terraform or CloudFormation. These tools are powerful, but they're not the only option.</p>
<p>Every modern cloud platform exposes an API. That API allows developers to create resources programmatically.</p>
<p>This means infrastructure can be controlled directly from code using HTTP requests or command-line interfaces.</p>
<p>Using APIs for IaC has several advantages.</p>
<p>First, it offers maximum flexibility. Developers can integrate infrastructure creation directly into applications, deployment scripts, or internal tools.</p>
<p>Second, it reduces tooling complexity. Instead of learning a specialized IaC language, teams can use languages they already know, such as Python, JavaScript, or Bash.</p>
<p>Third, it enables dynamic infrastructure. Scripts can create resources only when needed, scale them automatically, and remove them when work is complete.</p>
<p>For example, a test suite could automatically create a database, run tests, and delete the database afterwards. This keeps environments clean and reduces costs.</p>
<p>APIs essentially turn the cloud into a programmable platform.</p>
<h2 id="heading-automating-infrastructure-with-scripts">Automating Infrastructure with&nbsp;Scripts</h2>
<p>Using APIs for infrastructure automation usually follows a simple workflow.</p>
<ol>
<li><p>First, a script authenticates with the cloud platform using an API token or credentials.</p>
</li>
<li><p>Second, the script sends requests to create or modify resources such as applications, databases, or storage.</p>
</li>
<li><p>Third, the script captures identifiers or configuration values from the response.</p>
</li>
<li><p>Finally, those values are used in later steps, such as deployments or integrations.</p>
</li>
</ol>
<p>Because these steps run in code, they can easily be included in CI/CD pipelines.</p>
<p>A typical pipeline might do the following:</p>
<ul>
<li><p>Create infrastructure</p>
</li>
<li><p>Deploy the application</p>
</li>
<li><p>Run tests</p>
</li>
<li><p>Collect metrics</p>
</li>
<li><p>Destroy temporary environments</p>
</li>
</ul>
<p>This approach ensures every deployment follows the same process.</p>
<h2 id="heading-practical-example-with-sevalla">Practical Example with&nbsp;Sevalla</h2>
<p>A practical way to apply Infrastructure as Code through APIs is to use a command-line interface that directly interacts with a cloud platform’s API. This lets you automate infrastructure creation using scripts rather than dashboards.</p>
<p>One example is the <a href="https://github.com/sevalla-hosting/cli">Sevalla CLI</a>, which exposes infrastructure operations as terminal commands that can be executed manually or inside automation pipelines.</p>
<p><a href="https://sevalla.com/">Sevalla</a> is a developer-centric PaaS designed to simplify your workflow. They provide high-performance application hosting, managed databases, object storage, and static sites in one unified platform.</p>
<p>Other options are AWS and Azure, which require complex CLI tools and heavy DevOps overhead. Sevalla offers simplicity and ease of use, similar to Heroku.</p>
<h3 id="heading-installing-cli">Installing CLI</h3>
<p>You can install the CLI using the following shell command:</p>
<pre><code class="language-plaintext">bash &lt;(curl -fsSL https://raw.githubusercontent.com/sevalla-hosting/cli/main/install.sh)
</code></pre>
<p>Once installed, you can view the list of all available commands using the <code>help</code> command:</p>
<img src="https://cdn.hashnode.com/uploads/covers/66c6d8f04fa7fe6a6e337edd/3e6209cd-6c1e-420d-9d60-7376d54849d0.png" alt="Sevalla CLI Help" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The first step is authentication. Make sure you have an account on Sevalla before using the CLI.</p>
<pre><code class="language-plaintext">sevalla login
</code></pre>
<p>For automated environments such as CI/CD pipelines, authentication can be done with an API token. The token is stored in an environment variable so scripts can run without user interaction.</p>
<pre><code class="language-plaintext">export SEVALLA_API_TOKEN="your-api-token"
</code></pre>
<p>Once authenticated, you can quickly view a list of your apps using <code>sevalla apps list</code></p>
<img src="https://cdn.hashnode.com/uploads/covers/66c6d8f04fa7fe6a6e337edd/e3590cfa-5a95-4c5a-b0e9-8615070932da.png" alt="Sevalla Apps List" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-working-with-your-infrastructure-using-cli">Working with Your Infrastructure using CLI</h3>
<p>Your infrastructure can now be created directly from the command line. For example, you might start by creating an application service that will run the backend code:</p>
<pre><code class="language-plaintext">sevalla apps create --name myapp --source privateGit --cluster &lt;id&gt;
</code></pre>
<p>This command provisions a new application resource on the platform. Instead of navigating through a web interface and filling out forms, the entire setup is performed through a single command.</p>
<p>Because the command can be stored in scripts or configuration files, it becomes part of the project’s infrastructure definition.</p>
<p>After creating the application, you'll often need a database. You can also provision this programmatically:</p>
<pre><code class="language-plaintext">sevalla databases create \
  --name mydb \
  --type postgresql \
  --db-version 16 \
  --cluster &lt;id&gt; \
  --resource-type &lt;id&gt; \
  --db-name mydb \
  --db-password secret
</code></pre>
<p>This creates a PostgreSQL database with a defined version and credentials. In an automated workflow, the database creation step could run during environment setup for staging or testing.</p>
<p>Once the application and database exist, the next step might be configuring environment variables so the application can connect to the database:</p>
<pre><code class="language-plaintext">sevalla apps env-vars create &lt;app-id&gt; --key DATABASE_URL --value "postgres://..."
</code></pre>
<p>These configuration values can be injected during deployments, ensuring the application always receives the correct settings.</p>
<p>Deployment automation is another key part of Infrastructure as Code. Instead of manually triggering deployments, a script can deploy new code whenever a repository is updated.</p>
<pre><code class="language-plaintext">sevalla apps deployments trigger &lt;app-id&gt; --branch main
</code></pre>
<p>This allows CI/CD systems to deploy new versions of the application automatically after tests pass.</p>
<p>Infrastructure automation also includes scaling and monitoring. For example, if an application needs more instances to handle traffic, you can update the number of running processes programmatically.</p>
<pre><code class="language-plaintext">sevalla apps processes update &lt;process-id&gt; --app-id &lt;app-id&gt; --instances 3
</code></pre>
<p>You can also retrieve metrics through the CLI. This allows monitoring tools or scripts to analyze system performance.</p>
<pre><code class="language-plaintext">sevalla apps processes metrics cpu-usage &lt;app-id&gt; &lt;process-id&gt;
</code></pre>
<p>Similarly, you can also query application metrics such as response time or request rates to detect performance issues.</p>
<p>Another common step in infrastructure automation is configuring domains. Instead of manually linking domains to applications, a script can add them during environment setup.</p>
<pre><code class="language-plaintext">sevalla apps domains add &lt;app-id&gt; --name example.com
</code></pre>
<p>With these commands combined in scripts or pipelines, you can fully automate the lifecycle of your infrastructure. A CI pipeline could create an application, provision a database, configure environment variables, deploy code, attach a domain, and monitor performance  – all without human intervention.</p>
<p>Because every command supports JSON output, scripts can also capture values returned by the platform and reuse them in later steps. For example:</p>
<pre><code class="language-plaintext">APP_ID=$(sevalla apps list --json | jq -r '.[0].id')
</code></pre>
<p>This ability to chain commands together makes it easy to build powerful automation workflows.</p>
<p>In practice, teams often place these commands inside deployment scripts or pipeline steps. Whenever code is pushed to a repository, the pipeline automatically provisions or updates the infrastructure needed to run the application.</p>
<p>This approach demonstrates how APIs and automation tools can turn infrastructure into something you can manage the same way you manage application code: through scripts, version control, and automated workflows.</p>
<h2 id="heading-infrastructure-as-code-improves-developer-productivity">Infrastructure as Code Improves Developer Productivity</h2>
<p>One of the biggest benefits of Infrastructure as Code is developer productivity.</p>
<p>Developers no longer need to wait for infrastructure changes or manually configure environments.</p>
<p>Instead, infrastructure becomes part of the development workflow.</p>
<p>When a new feature requires a service, the developer simply adds the infrastructure definition to the repository. The pipeline then creates it automatically.</p>
<p>This reduces delays and keeps development moving quickly.</p>
<p>It also makes onboarding easier. New team members can spin up a full environment with a single command.</p>
<h2 id="heading-the-future-of-infrastructure">The Future of Infrastructure</h2>
<p>Cloud infrastructure continues to evolve toward automation and programmability.</p>
<p>Platforms increasingly expose APIs that allow every resource to be created, configured, and monitored through code.</p>
<p>This trend aligns naturally with the way developers already work.</p>
<p>Applications are built with code. Deployments are automated with code. It makes sense that infrastructure should also be defined with code.</p>
<p>Infrastructure as Code with APIs takes this idea even further. It allows infrastructure to be embedded directly into development workflows, pipelines, and internal tools.</p>
<p>The result is faster development, fewer configuration errors, and more reliable systems.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Infrastructure as Code has transformed how teams manage cloud environments.</p>
<p>By replacing manual configuration with code, organizations gain consistency, automation, and repeatability.</p>
<p>Using APIs to control infrastructure adds another level of flexibility. Developers can integrate infrastructure directly into scripts, pipelines, and applications.</p>
<p>This approach turns the cloud into a programmable platform.</p>
<p>As systems grow more complex and deployment cycles accelerate, the ability to automate infrastructure will only become more important.</p>
<p>For modern development teams, treating infrastructure as code is no longer optional. It's the foundation of reliable and scalable software delivery.</p>
<p><em>Hope you enjoyed this article. Learn more about me by</em> <a href="https://www.linkedin.com/in/manishmshiva/edit/intro/"><em><strong>visiting my LinkedIn</strong></em></a><em>.</em></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Create REST API Documentation in Node.js Using Scalar ]]>
                </title>
                <description>
                    <![CDATA[ A REST API documentation is a guide that explains how clients can make use of the REST APIs in an application. It details the available endpoints, how to send requests and what responses to expect. It ]]>
                </description>
                <link>https://www.freecodecamp.org/news/rest-api-documentation-with-scalar/</link>
                <guid isPermaLink="false">699f1d4e6049477bf67a9ad8</guid>
                
                    <category>
                        <![CDATA[ documentation ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ openai ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Orim Dominic Adah ]]>
                </dc:creator>
                <pubDate>Wed, 25 Feb 2026 16:03:26 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5fc16e412cae9c5b190b6cdd/ec5d63f1-d65f-4852-a297-9c517fc76e2f.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>A REST API documentation is a guide that explains how clients can make use of the REST APIs in an application. It details the available endpoints, how to send requests and what responses to expect. It may also contain explanations of concepts that are specific to the scope of the application.</p>
<p>Without API documentation, application development is considered incomplete because developers cannot build software to interact with it, rendering the application effectively useless.</p>
<p>In this article, you will learn how to create beautiful REST API documentation that also allows you to test the APIs for free using an OpenAPI specification and Scalar in Node.js projects. You will use <a href="https://github.com/asteasolutions/zod-to-openapi">asteasolutions/zod-to-openapi</a> to generate OpenAPI specification and use Scalar to create a web page from the specification.</p>
<p>To get the most out of this article, you should have experience developing REST APIs with Express or NestJS. You should also have experience with documenting REST APIs and using <a href="https://zod.dev/">zod</a>.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-rest-api-documentation-tools-for-nodejs">REST API Documentation Tools for Node.js</a></p>
<ul>
<li><p><a href="#heading-swagger">Swagger</a></p>
</li>
<li><p><a href="#heading-postman">Postman</a></p>
</li>
<li><p><a href="#heading-redoc">ReDoc</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-zod-to-openapi-and-scalar-for-rest-api-documentation">zod-to-openapi and Scalar for REST API Documentation</a></p>
<ul>
<li><a href="#heading-how-zod-to-openapi-works-with-scalar">How zod-to-openapi Works with Scalar</a></li>
</ul>
</li>
<li><p><a href="#heading-benefits-of-using-zod-to-openapi-and-scalar-to-create-rest-api-documentation">Benefits of Using zod-to-openapi and Scalar to Create REST API Documentation</a></p>
<ul>
<li><p><a href="#heading-openapi-specification-support">OpenAPI Specification Support</a></p>
</li>
<li><p><a href="#heading-open-source-and-free-to-use">Open Source and Free to Use</a></p>
</li>
<li><p><a href="#heading-better-documentation-experience">Better Documentation Experience</a></p>
</li>
<li><p><a href="#heading-developer-friendly-ui">Developer-friendly UI</a></p>
</li>
<li><p><a href="#heading-markdown-support">Markdown Support</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-how-to-create-the-api-documentation">How to Create the API Documentation</a></p>
<ul>
<li><p><a href="#heading-set-up-the-project">Set up the Project</a></p>
</li>
<li><p><a href="#heading-how-to-set-up-zod-to-openapi">How to Set Up zod-to-openapi</a></p>
</li>
<li><p><a href="#heading-how-to-generate-the-documentation-ui-with-scalar">How to Generate the Documentation UI with Scalar</a></p>
</li>
<li><p><a href="#heading-document-the-endpoints">Document the Endpoints</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-how-to-use-scalar-with-nestjs">How to Use Scalar with NestJS</a></p>
</li>
<li><p><a href="#heading-how-to-resolve-content-security-policy-csp-errors-when-used-with-helmet">How to Resolve Content Security Policy (CSP) Errors When Used with Helmet</a></p>
</li>
<li><p><a href="#heading-absence-of-asyncapi-documentation-feature">Absence of AsyncAPI Documentation Feature</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-rest-api-documentation-tools-for-nodejs">REST API Documentation Tools for Node.js</h2>
<p>A variety of tools already exist for documenting REST APIs and they have different strengths and weaknesses depending on their use case. While some of them are completely free to use, others operate a freemium model, and while some have an interface for testing APIs, others have a presentation-only user interface (UI).</p>
<p>Some of the most popular tools for documenting REST APIs in Node.js projects are listed below:</p>
<ul>
<li><p>Swagger</p>
</li>
<li><p>Postman</p>
</li>
<li><p>Redoc</p>
</li>
</ul>
<h3 id="heading-swagger">Swagger</h3>
<p>In order to document REST APIs in Express with Swagger, you need <a href="https://www.npmjs.com/package/swagger-jsdoc">swagger-jsdoc</a> and <a href="https://www.npmjs.com/package/swagger-ui-express">swagger-express-ui</a>. swagger-jsdoc collates and parses JSDoc-annotated documentation comments in the codebase and generates an OpenAPI specification document. swagger-ui-express uses the generated document to created a web page that renders the API documentation and test the APIs.</p>
<p>One of Swagger’s strengths lies in its support for the <a href="https://swagger.io/docs/specification/v3_0/about/">OpenAPI specification</a> which is an industry standard. Swagger is free to use, has a vibrant open source community and strong support for many programming languages and frameworks. It supports only REST APIs.</p>
<p>Its major drawback is the poor developer experience in manually writing JSDoc comments or YAML for the documentation. The process can be clumsy and developers can forget to include some annotations. Another drawback is that the JSDoc comments can interfere with reading functional code. Lastly, some developers have complained about its boring UI.</p>
<h3 id="heading-postman">Postman</h3>
<p><a href="https://www.postman.com/">Postman</a> is a cloud-based desktop API client application that allows developers and technical writers to write, test, collaborate on and publish API documentation. Unlike Swagger, it does not require deep programming experience to use most of its features. It is also not limited to REST API documentation — it can document APIs for GraphQL, websockets and gRPC.</p>
<p>Postman provides a UI to fill in details of an API documentation. The documentation process is manual and sometimes, its content can get out of sync with that of deployed applications. It is not free to use for teams and collaboration and hides the real behaviour of browser interaction with the APIs like CORS and streaming.</p>
<h3 id="heading-redoc">ReDoc</h3>
<p><a href="https://redocly.com/docs/redoc">ReDoc</a> is an open-source tool used to generate API documentation from an OpenAPI (Swagger) specification. It supports GraphQL, AsyncAPI and the OpenAPI specification and it renders a more beautiful documentation than Swagger. <a href="https://www.npmjs.com/package/redoc-express">redoc-express</a> is used to document Express REST APIs with Redoc.</p>
<p>Redoc’s major drawback is that its free community edition is presentation-only. It does not support testing the APIs. Similar to Swagger, a drawback it has is manually updating the application's OpenAPI document via a YAML specification file or JSDoc comments.</p>
<h2 id="heading-zod-to-openapi-and-scalar-for-rest-api-documentation">zod-to-openapi and Scalar for REST API Documentation</h2>
<p><a href="https://github.com/asteasolutions/zod-to-openapi">asteasolutions/zod-to-openapi</a> is a TypeScript library that generates OpenAPI specification from <a href="https://zod.dev/">zod</a> schemas. It provides typed methods which serve as guardrails for documenting API components instead of using code comments so that:</p>
<ul>
<li><p>The library methods serve as guardrails for what to document and how to document it</p>
</li>
<li><p>The documentation is consistent across the codebase</p>
</li>
<li><p>The documentation doesn't negatively affect code readability</p>
</li>
</ul>
<p>A sample snippet used to document a POST request for creating a user with zod-to-openapi is shown below:</p>
<pre><code class="language-typescript">// CreateUser and User are zod schema

registry.registerPath({
  method: "post",
  path: "/api/users",
  summary: "Create user",
  tags: ["users"],
  request: {
    body: {
      content: {
        "application/json": {
          schema: schema.CreateUser, 
        },
      },
      description: "Create user payload",
      required: true,
    },
  },
  responses: {
    201: {
      description: "User created",
      content: {
        "application/json": {
          schema: z.object({
            message: z.string(),
            data: schema.User,
          }),
        },
      },
    },
  },
});
</code></pre>
<p><a href="https://scalar.com/">Scalar</a> is a tool that generates beautiful, organized and searchable API documentation from OpenAPI documents. The documentation generated also supports testing the APIs and this makes Scalar effectively function as an API documentation generator and a lightweight API client. The image below shows a sample documentation generated by Scalar:</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/66e28b713f978a0e2cd2b763/56ca6e73-23a8-4245-a8d3-88c1f67b16be.svg" alt="Sample Scalar documentation UI" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h3 id="heading-how-zod-to-openapi-works-with-scalar">How zod-to-openapi Works with Scalar</h3>
<p>zod-to-openapi provides the functionality to generate an OpenAPI specification from code. Scalar uses the document generated to create a documentation web page that presents the information in the document in an organized and beautiful way that also allows for testing the APIs.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/66e28b713f978a0e2cd2b763/b327b21d-c80f-472c-ba6d-6c63d212faa8.png" alt="b327b21d-c80f-472c-ba6d-6c63d212faa8" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<h2 id="heading-benefits-of-using-zod-to-openapi-and-scalar-to-create-rest-api-documentation">Benefits of Using zod-to-openapi and Scalar to Create REST API Documentation</h2>
<p>When you combine zod-to-openapi and Scalar to create REST API documentation for your Express applications, you get a myriad of benefits. Some of the benefits are explained below:</p>
<h3 id="heading-openapi-specification-support">OpenAPI Specification Support</h3>
<p>The OpenAPI specification is a format for describing REST APIs. It takes takes into consideration the important components of an API necessary for clients to use it effectively. These components include:</p>
<ul>
<li><p>Request paths, methods, headers, path parameters and query parameters,</p>
</li>
<li><p>Schemas of request and response payloads,</p>
</li>
<li><p>Authentication requirements,</p>
</li>
<li><p>Descriptions for information not accommodated by other components</p>
</li>
</ul>
<p>zod-to-openapi provides methods for all of these to be included in the documentation and it generates an OpenAPI specification-compliant document that Scalar uses to generate the documentation web page.</p>
<h3 id="heading-open-source-and-free-to-use">Open Source and Free to Use</h3>
<p>zod-to-openapi is open source and free to use. It has no plans to be unsupported or sunsetted soon because like Ruby on Rails and Laravel, the creators of the project use it in their day-to-day work.</p>
<p>Scalar is open source too. It has paid plans but the features in the paid plans are only really useful for enterprise applications. The free version supports the necessary features needed to create useful REST API documentation.</p>
<h3 id="heading-better-documentation-experience">Better Documentation Experience</h3>
<p>In terms of user experience, the union of zod-to-openapi and Scalar provides the following benefits when writing documentation with both tools:</p>
<h4 id="heading-guardrails-with-zod-to-openapi-methods">Guardrails with zod-to-openapi Methods</h4>
<p>The methods provided by zod-to-openapi serve as guardrails to ensure that developers don't omit or forget the documentation of important components of APIs. The methods also ensure that these components are documented in an OpenAPI specification-compliant manner through the typed nature of the methods' parameters.</p>
<h4 id="heading-avoid-the-clumsiness-of-comments-and-yaml-files">Avoid the Clumsiness of Comments and YAML Files</h4>
<p>With zod-to-openapi, you don't document the APIs using comments or a YAML file. You document APIs using methods from zod-to-openapi. This removes the cluttering of code with comments and the clumsiness around manually updating large YAML files of OpenAPI specification.</p>
<h4 id="heading-accuracy-and-auto-generation-of-documentation">Accuracy and Auto-generation of Documentation</h4>
<p>When you use zod-to-openapi and Scalar, your API documentation is generated automatically when the application runs. zod-to-openapi does the collation and compilation of the documented APIs, and Scalar creates a web page for it that can be hosted on an API route of the same application. You don't need to manually run CLI commands to generate the documentation.</p>
<p>Another benefit of accuracy and auto-generation is that the job of API documentation is not split between technical writer and backend developer. The documentation is in the code and this makes development faster and more seamless in terms of API documentation.</p>
<h3 id="heading-developer-friendly-ui">Developer-friendly UI</h3>
<p>While Swagger’s UI is functional, some developers consider its presentation somewhat minimal, particularly when displaying detailed endpoint descriptions. ReDoc improves on visual design but does not offer API testing features. Scalar, on the other hand, delivers a more refined and intuitive interface with greater customization options than ReDoc.</p>
<p>Beyond its design advantages, Scalar provides auto-generated code samples in multiple programming languages. This enables developers to integrate APIs more efficiently using examples tailored to their specific tech stack.</p>
<p>Scalar's UI provides a search feature that allows developers to quickly locate specific sections of the API documentation. It also includes an AI chat interface that enables users to understand how different API endpoints can help address their specific use cases. This approach is more efficient than manually reviewing the entire documentation.</p>
<p>Lastly, you can test the API endpoints from the UI. When you make authenticated API requests, the UI caches authentication tokens so that you don't have to type or paste them for subsequent requests.</p>
<h3 id="heading-markdown-support">Markdown Support</h3>
<p>zod-to-openapi and Scalar have <a href="https://scalar.com/products/api-references/markdown">Markdown support</a>. With Markdown, you can include conceptual documentation and more information about API endpoints that are not supported by the default documentation components like headers and the request body.</p>
<p>You can embed images, include tables and format text in the documentation. You can use Markdown to include notes that explain concepts related to the API.</p>
<h2 id="heading-how-to-create-the-api-documentation">How to Create the API Documentation</h2>
<p>In this section, you will create an Express CRUD API project that uses zod-to-openapi and Scalar to document its APIs. To practice along, clone the Express starter project from GitHub at <a href="https://github.com/orimdominic/freeCodeCamp-zod-to-openapi-scalar#">orimdominic/freeCodeCamp-zod-to-openapi-scalar</a>.</p>
<h3 id="heading-set-up-the-project">Set up the Project</h3>
<p>After cloning the project:</p>
<ul>
<li><p>install its dependencies using your preferred Node.js package manager</p>
</li>
<li><p>start the server using the <code>serve</code> script</p>
</li>
</ul>
<pre><code class="language-shell"># Install dependencies
npm install

# Start the application
npm run serve
</code></pre>
<p>You should see the following output on the terminal if the application runs successfully:</p>
<pre><code class="language-shell">&gt; freecodecamp-zod-to-openapi-scalar@1.0.0 serve
&gt; node --experimental-strip-types --watch src/index.ts

Listening on :3000
</code></pre>
<p>The project has two modules - Users and Pets.</p>
<p>The router configuration for each module is defined in the <code>router.ts</code> file, while the route controllers are located in the <code>controllers.ts</code> file within each module's folder under <code>src/modules</code>. The controllers do not contain business logic, they simply respond with JSON values generated by the <a href="https://fakerjs.dev/">Faker</a> library.</p>
<h3 id="heading-how-to-set-up-zod-to-openapi">How to Set Up zod-to-openapi</h3>
<p>Install <a href="https://github.com/asteasolutions/zod-to-openapi">asteasolutions/zod-to-openapi</a> using your preferred Node.js package manager. If you use npm, run the code snippet below in your terminal:</p>
<pre><code class="language-shell">npm install @asteasolutions/zod-to-openapi
</code></pre>
<p>After the installation, create a folder called <code>lib</code> (library) in the <code>src</code> folder. In the <code>lib</code> folder, create a file called <code>openapi.ts</code>. The file will house the code that sets up zod-to-openapi for collating the API documentation and generating the OpenAPI specification.</p>
<p>Copy and paste the code snippet below into <code>src/lib/openapi.ts</code>:</p>
<pre><code class="language-typescript">import z from "zod";
import {
  extendZodWithOpenApi,
  OpenApiGeneratorV31,
  OpenAPIRegistry,
} from "@asteasolutions/zod-to-openapi";

extendZodWithOpenApi(z);

export const registry = new OpenAPIRegistry();

export const bearerAuth = registry.registerComponent("securitySchemes", "bearerAuth", {
  type: "http",
  scheme: "bearer",
  bearerFormat: "JWT",
});

export function generateOpenAPIDocument() {
  const generator = new OpenApiGeneratorV3(registry.definitions);

  return generator.generateDocument({
    openapi: "3.1.0",
    info: {
      title: "Users API",
      version: "1.0.0",
      description: `Backend API documentation for users application.`,
    },
    tags: [
      {
        name: "users",
        description: "For operations carried out by admin users",
      },
    ],
    servers: [
      {
        url: "http://localhost:3000",
        description: "Local server",
      },
    ],
  });
}
</code></pre>
<blockquote>
<p>zod-to-openapi v8 requires zod v4. If you use zod v3, you should use v7.3.4 of zod-to-openapi.</p>
</blockquote>
<p><code>extendZodWithOpenApi</code> is a method provided by <code>zod-to-openapi</code> that enhances Zod schemas by adding an <code>openapi</code> method. The <code>openapi</code> method allows you to attach additional documentation to request payloads, responses, parameters, and their properties, which are then displayed in the API documentation rendered by Scalar.</p>
<p>It is important to call <code>extendZodWithOpenApi</code> before loading any files that use the <code>openapi</code> method, otherwise accessing <code>openapi</code> on Zod objects will result in errors.</p>
<p>An alternative is to use the <code>meta</code> method on zod v4 schemas for the additional documentation. For example, <code>schemaOne</code> and <code>schemaTwo</code> in the code snippet below are the same:</p>
<pre><code class="language-typescript">const schemaOne = z
  .string()
  .openapi({ description: 'Name of the user', example: 'Test' });

const schemaTwo = z
  .string()
  .meta({description: 'Name of the user', example: 'Test' });
</code></pre>
<p>The <code>meta</code> method supports all metadata information that you'd normally pass to <code>openapi</code> and will produce exactly the same results.</p>
<p>The <code>OpenAPIRegistry</code> is a utility that is used to collate API documentation which would later be passed to an OpenAPI specification generator. <code>registry</code> is created from <code>OpenAPIRegistry</code>, exported, and used to document API endpoints and components in modules where it is imported.</p>
<pre><code class="language-plaintext">export const registry = new OpenAPIRegistry();
</code></pre>
<p><code>bearerAuth</code> is a component created by the <code>registry</code> to represent JWT authentication. When <code>bearerAuth</code> is included in the documentation of an endpoint, the UI renders an input for submitting an authentication token for authenticated requests as shown in the image below.</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/66e28b713f978a0e2cd2b763/2bfe72ca-a5ee-4ce5-ba5f-6ed6e1c52774.png" alt="Scalar input UI for submitting JWT" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>In the <code>registerComponent</code> method, <code>"securitySchemes"</code> registers a security scheme component. <code>"bearerAuth"</code> , the first argument of <code>registerComponent</code>, is the name given to the component and it can be changed to a name that you prefer. It appears in the top right of the authentication token input, shown in the image above. The third input to <code>registerComponent</code> is an object that defines the component.</p>
<p>When <code>generateOpenAPIDocument</code> function is executed, it collates all the registry API definitions in the project, generates the OpenAPI specification through <code>generator.generateDocument</code>, and returns the specification as JSON.</p>
<p>The <code>tags</code> property in <code>generator.generateDocument</code> organizes API endpoints into sections on the documentation UI. For example, all API endpoints with the <code>Users</code> tag in their registry definition will be placed under the <code>Users</code> section of the UI. <code>description</code> can be written in Markdown within template literals.</p>
<p>The <code>servers</code> property is a collection of the servers connected to the application. If you have multiple servers, you have the option of selecting what server to use for the base URL in the documentation UI for making API requests from it.</p>
<p>With this setup in place, when endpoints are documented with the registry, <code>generateOpenAPIDocument</code> will have an OpenAPI specification to return.</p>
<h3 id="heading-how-to-generate-the-documentation-ui-with-scalar">How to Generate the Documentation UI with Scalar</h3>
<p>In this section, you will set up Scalar and connect it to the return value of <code>generateOpenAPIDocument</code>. You will also connect Scalar with an Express route, allowing the application to serve the documentation UI at that route.</p>
<p>Scalar has an <a href="https://scalar.com/products/api-references/integrations/express">Express API reference</a> library that makes it easier for you to connect it with the OpenAPI specification and Express. Install <code>scalar/express-api-reference</code> using your preferred Node.js package manager. If you use npm, use the snippet below:</p>
<pre><code class="language-shell">npm install @scalar/express-api-reference 
</code></pre>
<p>Copy and paste the code snippet below into <code>src/app.js</code>:</p>
<pre><code class="language-typescript">import express from "express";
import router from "./router.ts";
import { generateOpenAPIDocument } from "./lib/openapi.ts";
import { apiReference } from "@scalar/express-api-reference";

const app = express();
app.use(express.json(), express.urlencoded({ extended: true }));

app.get("/", function (req, res) {
  return res.send("OK");
});

app.use("/api", router);

const apiDocJsonContent = generateOpenAPIDocument();

app.use(
  "/docs", // documentation route
  apiReference({
    content: apiDocJsonContent,
    title: "Users API",
    pageTitle: "Users API",
  }),
);

export default app;
</code></pre>
<p>In the code snippet above, <code>generateOpenAPIDocument</code> is imported from <code>src/lib/openapi.ts</code>, and <code>apiReference</code> is imported from <code>@scalar/express-api-reference</code>. When executed, <code>generateOpenAPIDocument</code> returns the OpenAPI specification, which is stored in <code>apiDocJsonContent</code> for caching to improve perfoemance.</p>
<p>A <code>GET /docs</code> route is then created, with the Scalar <code>apiReference</code> function acting as the controller. It accepts <code>apiDocJsonContent</code> and returns a web page whenever the <code>GET /docs</code> route is accessed.</p>
<p>With this setup in place, run the application using <code>npm run serve</code> and visit the documentation page at <code>http://localhost:3000/docs</code> in your browser. You should see a user interface similar to the image below:</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/66e28b713f978a0e2cd2b763/387698d3-ddcc-4263-8377-428399010892.png" alt="Scalar documentation starter UI" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>To view the codebase at this point, run <code>git checkout set-up-openapi-scalar</code> .</p>
<h3 id="heading-document-the-endpoints">Document the Endpoints</h3>
<p>You have set up zod-to-openapi and connected it with Scalar. You have also hooked it up with a route in the backend application. In this section, you will write code to document the endpoints in the application for generating the OpenAPI specification and rendering it on the documentation UI.</p>
<p>To document the route for creating users ( <code>POST /api/users</code> ), in <code>src/modules/users/router.ts</code> , import <code>registry</code>, the schemas and zod using the snippet below:</p>
<pre><code class="language-typescript">import z from "zod";
import { registry } from "../../lib/openapi.ts";
import { 
    UserSchema, 
    UserListItemSchema,
    UpdateUserSchema, 
    CreateUserSchema, 
} from "./types.ts";
</code></pre>
<p>Copy and paste the code below above the create user route to document the create user endpoint:</p>
<pre><code class="language-typescript">registry.registerPath({
  method: "post",
  path: "/api/users",
  summary: "Create user",
  tags: ["users"],
  request: {
    body: {
      content: {
        "application/json": {
          schema: CreateUserSchema,
        },
      },
      description: "Create user payload",
      required: true,
    },
  },
  responses: {
    201: {
      description: "User created",
      content: {
        "application/json": {
          schema: z.object({
            message: z.string().openapi({ example: "User created" }),
            data: UserSchema,
          }),
        },
      },
    },
  },
});
</code></pre>
<p>Visit the documentation page and you will find see a web page similar to the image below:</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/66e28b713f978a0e2cd2b763/22e0f15f-a7ee-4806-ab86-cfe922a3feda.png" alt="Scalar documentation UI for the create user route" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The UI result of some of the input fields of <code>registry.registerPath</code> have been labelled in the image above. The description in the API endpoint is italicised because its value is Markdown in a template string.</p>
<p>By registering the route path for creating users with <code>registry.registerPath</code> and filling its values, you added the documentation of the route to the registry definitions and that makes it included in the OpenAPI specification.</p>
<p>To test the endpoint from the documentation UI:</p>
<ul>
<li><p>click the <em>Test Request</em> button</p>
</li>
<li><p>fill in the payload in the dialog that appears and</p>
</li>
<li><p>click the <em>Send</em> button</p>
</li>
</ul>
<p>To document the <code>get</code> user by <code>id</code> route (<code>GET /api/users/:id</code> ), import <code>bearerAuth</code> from <code>src/lib/openapi.ts</code>, copy the code snippet below and paste it above the get user by id route definition.</p>
<pre><code class="language-typescript">registry.registerPath({
  method: "get",
  path: "/api/users/{userId}",
  summary: "Get user details by id",
  tags: ["Users"],
  security: [{ [bearerAuth.name]: [] }],
  request: {
    params: z.object({ userId: z.int() }),
  },
  responses: {
    200: {
      description: "User retrieved",
      content: { "application/json": { schema: UserSchema } },
    },
  },
});
</code></pre>
<p>When the <code>request.params</code> field is defined using a Zod object, it generates an input UI on the documentation web page that enables users to provide values for path parameters such as <code>userId</code>, highlighted in the image below:</p>
<img src="https://cloudmate-test.s3.us-east-1.amazonaws.com/uploads/covers/66e28b713f978a0e2cd2b763/ff4db093-580b-47eb-bd91-2216966566c0.png" alt="Request path parameter from registry definition on Scalar" style="display:block;margin:0 auto" width="600" height="400" loading="lazy">

<p>The complete code for the documentation of all endpoints in this section can be accessed when you check out the <code>complete-project</code> branch by running <code>git checkout complete-project</code> in your terminal. It contains documentation for the endpoint for uploading user photo, which demonstrates how to document endpoints that accept file uploads.</p>
<h2 id="heading-how-to-use-scalar-with-nestjs">How to Use Scalar with NestJS</h2>
<p>Scalar has a library that integrates with NestJS. You can use supply the Swagger document created by <a href="https://docs.nestjs.com/openapi/introduction">swagger/nestjs</a> to the <a href="https://scalar.com/products/api-references/integrations/nestjs">Scalar NestJS integration library</a> to generate the Scalar documentation UI.</p>
<p>In root folder of your NestJS project, install the Scalar NestJS integration library:</p>
<pre><code class="language-shell">npm install @scalar/nestjs-api-reference
</code></pre>
<p>Update the <code>main.ts</code> file of your NestJS project with the code snippet below:</p>
<pre><code class="language-typescript">import { NestFactory } from '@nestjs/core';
import { apiReference } from '@scalar/nestjs-api-reference';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const options = new DocumentBuilder()
    .setTitle('Cats example')
    .setDescription('The cats API description')
    .setVersion('1.0')
    .addTag('cats')
    .addBearerAuth()
    .build();

  const openApiSpecification = SwaggerModule.createDocument(app, options);
  
  // integrate the documentation with NestJS
  app.use(
    '/api/docs', // documentation route
    apiReference({
      content: openApiSpecification,
    }),
  )

  await app.listen(3000);
  console.log(`Application is running on: ${await app.getUrl()}`);
}

bootstrap();
</code></pre>
<p>With this setup in place, you can visit the <code>/api/docs</code> route in your browser to view the Scalar documentation for your NestJS application.</p>
<h2 id="heading-how-to-resolve-content-security-policy-csp-errors-when-used-with-helmet">How to Resolve Content Security Policy (CSP) Errors When Used with Helmet</h2>
<p>If you use <a href="https://www.npmjs.com/package/helmet">Helmet</a> in your Express or NestJS project, you will encounter <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP">CSP</a> errors when you try to render the Scalar documentation UI. To resolve the errors, update the Helmet CSP configuration in your code to have the value of the object in the code snippet below:</p>
<pre><code class="language-typescript">{
    directives: {
      defaultSrc: [`'self'`],
      styleSrc: [`'self'`, `'unsafe-inline'`],
      imgSrc: [`'self'`, 'data:', 'validator.swagger.io'],
      scriptSrc: ['self', 'https:', 'unsafe-inline'],
    },
  }
</code></pre>
<h2 id="heading-absence-of-asyncapi-documentation-feature">Absence of AsyncAPI Documentation Feature</h2>
<p>At the time of writing, Scalar does not fully support rendering AsyncAPI specifications for event-driven architecture APIs, although it is currently under development. You can track the progress of its development through the GitHub issue linked in the documentation to stay informed about its release.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You have learned about zod-to-openapi and how it makes it easier for you to generate an OpenAPI specification for your REST APIs than writing comments or large YAML files. You also learned how to use the specification document generated to render a beautiful API documentation UI which also functions as a lightweight API client. Endeavour to implement it in your projects that need a documentation uplift.</p>
<p>Feel free to <a href="https://www.linkedin.com/in/orimdominicadah/">connect with me on LinkedIn</a> for questions or clarifications. Thank you for reading this far and I hope this helps you achieve what you intended to achieve. Don’t hesitate to share this article if you feel that it would help someone else out there. Cheers!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Why Your UI Won’t Update: Debugging Stale Data and Caching in React Apps ]]>
                </title>
                <description>
                    <![CDATA[ Your UI doesn’t “randomly” refuse to update. In most cases, it’s rendering cached data, which is data that was saved somewhere so the app doesn’t have to do the same work again. Caching is great for performance, but it becomes a pain when you don’t r... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/why-your-ui-wont-update-debugging-stale-data-and-caching-in-react-apps/</link>
                <guid isPermaLink="false">6984d41160b1e5f9aeccaa9e</guid>
                
                    <category>
                        <![CDATA[ caching ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Frontend Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Oluwadamisi Samuel ]]>
                </dc:creator>
                <pubDate>Thu, 05 Feb 2026 17:32:01 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770312709391/8442f6df-1133-47f7-a035-02c958145811.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Your UI doesn’t “randomly” refuse to update. In most cases, it’s rendering cached data, which is data that was saved somewhere so the app doesn’t have to do the same work again.</p>
<p>Caching is great for performance, but it becomes a pain when you don’t realize which layer is reusing old data.</p>
<p>If you’ve ever seen this:</p>
<ul>
<li><p>You update a profile name, but the screen still shows the old one.</p>
</li>
<li><p>You delete an item, but it stays in the list.</p>
</li>
<li><p>Your API returns fresh JSON, but the page refuses to change.</p>
</li>
<li><p>You deploy a fix, but your teammate still sees the old behavior.</p>
</li>
</ul>
<p>You’re probably hitting a cache.</p>
<p>What makes this especially confusing is that not all stale UI comes from “real” caches. Modern web apps have multiple places where data can be reused, saved, or replayed between your UI, your API and when your app is deployed. When you don’t have a clear mental model of these layers, debugging turns into guesswork.</p>
<p>This article lays out a practical guide of the five most common caching layers that cause stale UI, plus one non-cache trap that looks exactly like one. The goal is to help you quickly identify where stale data is coming from, so you can fix the right thing instead of “refreshing harder.”</p>
<h2 id="heading-why-it-matters">Why it Matters</h2>
<p>I first ran into this while building an app where the UI wouldn’t update after a successful change. The API returned 200 OK, the database was correct, but the screen stayed stale. I assumed something was wrong with my code or state logic. Instead, the issue was coming from a caching layer I hadn’t invalidated. That’s the real problem with stale UI, you can’t debug it effectively unless you know which layer might be serving cached data.</p>
<p>When you understand where caching happens:</p>
<ul>
<li><p>You debug faster by identifying the layer instead of guessing.</p>
</li>
<li><p>You avoid production-only bugs caused by caching defaults.</p>
</li>
<li><p>You stop chasing React issues when the data was never fresh.</p>
</li>
</ul>
<p>This article gives you a simple mental model to pinpoint the layer and fix the right thing.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-why-it-matters">Why it Matters</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-mental-model">The Mental Model</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-non-cache-cause">Non-Cache Cause</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-cache-1-react-query-cache">Cache 1: React Query Cache</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-cache-2-nextjs-fetch-caching">Cache 2: Next.js fetch() Caching</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-cache-3-browser-http-cache-a-saved-copy-in-your-browser">Cache 3: Browser HTTP Cache (a Saved Copy in Your Browser)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-cache-4-cdnhosting-cache">Cache 4: CDN/Hosting Cache</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-cache-5-service-worker-cache-only-if-your-site-is-a-pwa">Cache 5: Service Worker Cache (Only if Your Site is a PWA)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-10-second-debug-guide">10-Second Debug Guide</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-prevention-set-caching-intentionally">Prevention: Set Caching Intentionally</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-recap">Recap</a></p>
</li>
</ul>
<h2 id="heading-the-mental-model">The Mental Model</h2>
<p>When your UI shows data, it feels like it comes straight from your API. In reality, the request/response path can hit multiple reuse points.</p>
<h2 id="heading-non-cache-cause">Non-Cache Cause</h2>
<p>Duplicated React local state (same symptoms as caching). This one isn’t a formal cache, but it causes a lot of “why didn’t it update?” bugs especially for beginners.</p>
<h3 id="heading-the-common-trap">The common trap:</h3>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> [name, setName] = useState(user.name) <span class="hljs-comment">// initialized once</span>
</code></pre>
<p><code>useState</code> only uses its argument during the initial render. On every subsequent render, React ignores this value and preserves the existing state.</p>
<p>If <code>user.name</code> later changes (for example, after fresh API data arrives), the <code>name</code> state will not update automatically. At that point, <code>name</code> becomes a stale copy of <code>user.name</code>, and the UI renders outdated data unless you manually synchronize it.</p>
<p>This happens because you have duplicated state:</p>
<ul>
<li><p><code>user.name</code> is the source of truth.</p>
</li>
<li><p><code>name</code> state is a local snapshot taken once.</p>
</li>
</ul>
<p>React does not keep duplicated state in sync for you.</p>
<p>Correct patterns:</p>
<ol>
<li>Render directly from the source when possible.</li>
</ol>
<p>If the value is not being edited locally, do not copy it into state:</p>
<pre><code class="lang-javascript">&lt;span&gt;{user.name}&lt;/span&gt;
</code></pre>
<p>This guarantees the UI always reflects the latest data.</p>
<ol start="2">
<li>Explicitly synchronize local state when editable state is required.</li>
</ol>
<p>If you need local, editable state (for example, a controlled input), you must opt in to synchronization:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> [name, setName] = useState(user.name);  

    useEffect(<span class="hljs-function">() =&gt;</span> {    
        setName(user.name); 
     }, [user.name]);
</code></pre>
<p>This effect runs only when <code>user.name</code> changes, explicitly updating local state to match the new source value.</p>
<h2 id="heading-cache-1-react-query-cache">Cache 1: React Query Cache</h2>
<p>React Query (TanStack Query) stores query results in a QueryClient cache (in memory by default) so your UI can render quickly and avoid unnecessary network requests. When a component needs data, React Query can return cached data immediately and then decide whether to fetch the data again based on options like <code>staleTime</code> and “refetch” behaviors (on mount, window focus, reconnect).</p>
<h3 id="heading-common-failure-mode-mutation-succeeds-but-the-ui-stays-old">Common failure mode: mutation succeeds, but the UI stays old</h3>
<p>A 200 OK only confirms the mutation request succeeded. It does not automatically update the cached query data your UI is rendering.</p>
<p>After a mutation, one of these usually happens:</p>
<ul>
<li><p>The query that renders the screen was not invalidated/fetched</p>
</li>
<li><p>You invalidated the wrong query key (the UI reads from a different key)</p>
</li>
<li><p>The UI is rendering local React state that’s out of sync (not the query result)</p>
</li>
</ul>
<p>The simplest “safe” pattern is: invalidate the exact query key your UI uses, so it fetches fresh data.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { useMutation, useQueryClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@tanstack/react-query"</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useUpdateProfile</span>(<span class="hljs-params">userId: string</span>) </span>{
  <span class="hljs-keyword">const</span> queryClient = useQueryClient();

  <span class="hljs-keyword">return</span> useMutation({
    <span class="hljs-attr">mutationFn</span>: updateProfileRequest,
    <span class="hljs-attr">onSuccess</span>: <span class="hljs-function">() =&gt;</span> {
      <span class="hljs-comment">// Invalidate the same key your UI query uses (example: ["user", userId])</span>
      queryClient.invalidateQueries({ <span class="hljs-attr">queryKey</span>: [<span class="hljs-string">"user"</span>, userId] });
    },
  });
}
</code></pre>
<p>If your UI uses a different key (for example <code>["me"]</code> or <code>["user", userId, "profile"]</code>), you must invalidate that key instead, React Query won’t “figure it out” from the URL.</p>
<h3 id="heading-query-keys-react-query-caches-by-key-not-url">Query Keys: React Query Caches by Key, not URL</h3>
<p>React Query does not cache by endpoint URL. The query key is the identity of the cached data. If two different requests share the same key, React Query treats them as the same data and they can overwrite each other.</p>
<p>You should avoid keys like <code>["user"]</code> (too broad), and use keys like <code>["user", userId]</code> and <code>["users", { page, search, filter }]</code>.</p>
<p><strong>Two settings that control “when it will refetch”:</strong></p>
<ul>
<li><p><strong>staleTime:</strong> how long cached data is treated as fresh. While data is fresh, React Query is less likely to refetch automatically.</p>
</li>
<li><p><strong>gcTime (formerly cacheTime):</strong> how long unused query data stays in memory after it’s no longer used by any component, before it’s garbage collected.</p>
</li>
</ul>
<h2 id="heading-cache-2-nextjs-fetch-caching">Cache 2: Next.js fetch() Caching</h2>
<p>This is the one that surprises a lot of frontend devs. Next.js can cache results to speed things up. That means your server might return a previously saved copy of:</p>
<ul>
<li><p>The API data it fetched, or</p>
</li>
<li><p>The page it already built</p>
</li>
</ul>
<p>This is often the first time frontend developers encounter server-side caching behavior that affects UI correctness. So, even if your database has the new value, you can still see the old one, because Next.js didn’t fetch the API again, or didn’t rebuild the page this time.</p>
<p>This mainly applies to the App Router (Next.js calls these saved copies the Data Cache and Full Route Cache).</p>
<h3 id="heading-what-youll-notice-when-this-happens">What you’ll notice when this happens</h3>
<ul>
<li><p>You refresh the page and it still shows the old value.</p>
</li>
<li><p>Your API is correct (Postman/curl shows the new email), but the UI is stuck.</p>
</li>
<li><p>Sometimes it “fixes itself” after a short wait (because the saved copy refreshes on a timer).</p>
</li>
</ul>
<p>For example: “I updated my profile email, but prod still shows the old one”</p>
<p>The page (reads email on the server):</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// app/settings/page.tsx</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">SettingsPage</span>(<span class="hljs-params"></span>) </span>{
 <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"https://api.example.com/users/42"</span>, {
  <span class="hljs-attr">method</span>: <span class="hljs-string">"GET"</span>,
})
  <span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> res.json();

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">main</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h1</span>&gt;</span>Settings<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Email: {user.email}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">main</span>&gt;</span></span>
  );
}
</code></pre>
<p>You submit an “Update email” form, the API returns <strong>200 OK</strong>, the database is updated, but /settings still shows the previous email in production.</p>
<p>That usually means you’re seeing a saved copy somewhere on the server side.</p>
<h3 id="heading-how-to-debug-it">How to debug it</h3>
<h4 id="heading-step-1-reproduce-in-a-production-like-run">Step 1: Reproduce in a production-like run</h4>
<p>Caching can behave differently in development. Run:</p>
<pre><code class="lang-bash">next build &amp;&amp; next start
</code></pre>
<p>Then test again.</p>
<h4 id="heading-step-2-confirm-whether-the-request-is-reaching-your-nextjs-server-at-all">Step 2: Confirm whether the request is reaching your Next.js server at all</h4>
<p>Add a log inside the page:</p>
<pre><code class="lang-javascript"><span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Rendering /settings at"</span>, <span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>().toISOString());
</code></pre>
<p>Then reload settings twice.</p>
<ul>
<li><p>If you see a new timestamp every reload, the request is reaching your server and the page code is running.</p>
</li>
<li><p>If you don’t see logs in production, your request may not be reaching your server at all (often because a hosting/CDN layer is serving a saved copy before Next.js runs). You’ll confirm that in the CDN section later.</p>
</li>
</ul>
<h4 id="heading-step-3-force-nextjs-to-ask-your-api-every-time">Step 3: Force Next.js to ask your API every time</h4>
<p>Change the fetch to:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"https://api.example.com/me"</span>, {
  <span class="hljs-attr">method</span>: <span class="hljs-string">"GET"</span>,
  <span class="hljs-attr">cache</span>: <span class="hljs-string">"no-store"</span>,
});
</code></pre>
<p>This means: don’t save this response – always fetch it again.</p>
<p>If this fixes the stale email then the problem was a saved copy of the API response (Data Cache).</p>
<h4 id="heading-step-4-if-the-email-is-still-stale-force-nextjs-to-rebuild-the-page-every-request">Step 4: If the email is still stale, force Next.js to rebuild the page every request</h4>
<p>Add this to the page file:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// app/settings/page.tsx</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> dynamic = <span class="hljs-string">"force-dynamic"</span>;
</code></pre>
<p>This means: don’t serve a saved copy of the page; rebuild it per request.</p>
<h3 id="heading-a-beginner-safe-setup-for-the-user-settings-pages-with-some-of-the-suggestions">A “beginner-safe” setup for the user settings pages with some of the suggestions:</h3>
<pre><code class="lang-javascript"><span class="hljs-comment">// app/settings/page.tsx</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> dynamic = <span class="hljs-string">"force-dynamic"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">SettingsPage</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"https://api.example.com/me"</span>, { <span class="hljs-attr">cache</span>: <span class="hljs-string">"no-store"</span> });
  <span class="hljs-keyword">const</span> me = <span class="hljs-keyword">await</span> res.json();
  <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Email: {me.email}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span></span>;
}
</code></pre>
<p>When you want caching for speed, but still need real time updates, these are some options you can take:</p>
<h4 id="heading-option-a-refresh-the-saved-copy-every-n-seconds">Option A: Refresh the saved copy every N seconds</h4>
<p>Good for public pages, not ideal for “my settings must update now.”</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">await</span> fetch(url, { <span class="hljs-attr">next</span>: { <span class="hljs-attr">revalidate</span>: <span class="hljs-number">60</span> } });
</code></pre>
<p>This means: “You can reuse a saved copy, but refresh it at most every 60 seconds.”</p>
<h4 id="heading-option-b-refresh-right-after-the-update-best-for-update-email-flows">Option B: Refresh right after the update (best for “update email” flows)</h4>
<p>If you update the email on the server (Server Action or API route), tell Next.js to throw away the saved copy for /settings page so the next visit is fresh:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// app/settings/actions.ts</span>
<span class="hljs-string">"use server"</span>;

<span class="hljs-keyword">import</span> { revalidatePath } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/cache"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">updateEmail</span>(<span class="hljs-params">email: string</span>) </span>{
  <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">"https://api.example.com/me/email"</span>, {
    <span class="hljs-attr">method</span>: <span class="hljs-string">"PUT"</span>,
    <span class="hljs-attr">headers</span>: { <span class="hljs-string">"content-type"</span>: <span class="hljs-string">"application/json"</span> },
    <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({ email }),
  });

  <span class="hljs-comment">// Tell Next.js: next request to /settings should be rebuilt</span>
  revalidatePath(<span class="hljs-string">"/settings"</span>);
}
</code></pre>
<p><strong>Note</strong>: Next.js caching details can differ by version and by App Router vs Pages Router. Instead of trying to memorize defaults, debug by setting the behavior explicitly (no-store, revalidate, force-dynamic) and observe what changes.</p>
<h2 id="heading-cache-3-browser-http-cache-a-saved-copy-in-your-browser">Cache 3: Browser HTTP Cache (a Saved Copy in Your Browser)</h2>
<p>Sometimes the browser reuses a saved copy of an API response (from memory or disk), so it doesn’t fully fetch it again.</p>
<h3 id="heading-what-youll-notice">What you’ll notice</h3>
<p>You open DevTools, and the network shows (from memory cache) or (from disk cache).</p>
<h3 id="heading-fast-check">Fast check</h3>
<p>DevTools → Network</p>
<ul>
<li><p>Turn on Disable cache (only works while DevTools is open)</p>
</li>
<li><p>Reload and retry</p>
</li>
</ul>
<h3 id="heading-why-it-happens">Why it happens</h3>
<p>Usually your server allows caching via headers like Cache-Control or ETag (which can lead to 304 Not Modified).</p>
<h2 id="heading-cache-4-cdnhosting-cache">Cache 4: CDN/Hosting Cache</h2>
<p>This is often a production-only cache, which is why frontend bugs can appear “impossible” to reproduce locally. In production, a CDN/hosting layer can serve a saved copy of a response before your request reaches your server. That’s why “prod is stale, local is fine” happens.</p>
<h3 id="heading-what-youll-notice-1">What you’ll notice</h3>
<ul>
<li><p>Prod is stale, local is fine</p>
</li>
<li><p>Different users see different results (different regions/POPs)</p>
</li>
<li><p>Pages are very fast even right after data changed</p>
</li>
</ul>
<h3 id="heading-fast-check-1">Fast check</h3>
<p>Open DevTools → Network → click the request → Response Headers</p>
<ul>
<li><p>Age: if present and increasing, it’s strong evidence you’re getting a cached response from an intermediary cache</p>
</li>
<li><p>Provider headers can hint HIT/MISS (examples: x-vercel-cache, cf-cache-status)</p>
</li>
<li><p>Source (Age header, HTTP caching): <a target="_blank" href="https://www.rfc-editor.org/rfc/rfc9111">https://www.rfc-editor.org/rfc/rfc9111</a></p>
</li>
</ul>
<h3 id="heading-quick-diagnostic-check">Quick diagnostic check</h3>
<p>Change the URL slightly by adding this to the end of the URL:</p>
<pre><code class="lang-javascript">?debug=<span class="hljs-number">1700000000000</span>
</code></pre>
<p>If the new URL shows fresh data, the edge was likely caching the original URL. This doesn’t fix it for everyone, you’d still need correct cache settings or a purge/invalidation on your CDN.</p>
<h2 id="heading-cache-5-service-worker-cache-only-if-your-site-is-a-pwa">Cache 5: Service Worker Cache (Only if Your Site is a PWA)</h2>
<p>If your site has a service worker, it can return a saved response before the network runs. This can make new deployments or new data seem “ignored.”</p>
<h3 id="heading-what-youll-notice-2">What you’ll notice</h3>
<ul>
<li><p>Works in Incognito but not normal mode</p>
</li>
<li><p>Hard refresh doesn’t help</p>
</li>
<li><p>DevTools “Disable cache” doesn’t fully explain it</p>
</li>
</ul>
<h3 id="heading-fast-check-chrome">Fast check (Chrome)</h3>
<p>Open DevTools → Application → Service Workers</p>
<ul>
<li><p>enable Bypass for network, or Unregister temporarily</p>
</li>
<li><p>reload and retest</p>
</li>
</ul>
<h2 id="heading-10-second-debug-guide">10-Second Debug Guide</h2>
<p>Stale data is rarely random: it usually means a cache layer is doing its job, just not in the way you expect. Modern applications stack multiple caches, so debugging is less about fixing code immediately and more about locating the layer responsible.</p>
<p>Think of this as a quick cheat sheet to figure out which cache layer might be serving stale data, so you can focus your debugging on the right layer.</p>
<ul>
<li><p>No request in Network? Go to <code>Cache 1 (React Query)</code>, then Local state, then <code>Cache 5 (Service worker)</code>.</p>
</li>
<li><p>Request exists, but response is old? Go to <code>Cache 3 (Browser)</code>, <code>Cache 4 (CDN)</code>, then <code>Cache 2 (Next.js)</code>.</p>
</li>
<li><p>Response is fresh, UI is old? Go back to <code>Cache 1 (invalidating / query keys)</code> and Local state.</p>
</li>
</ul>
<p>Once you know the likely layer, use the Fast check in that section to confirm it.</p>
<h2 id="heading-prevention-set-caching-intentionally">Prevention: Set Caching Intentionally</h2>
<p>Most stale-data bugs happen because caching settings were never chosen but the defaults were.</p>
<ul>
<li><p>User-specific pages (settings/admin/dashboard): default to fresh: Next.js: use cache: "no-store" on important fetches, and/or force dynamic routes when needed.</p>
</li>
<li><p>Public pages (marketing/blog/docs): saving + revalidate is usually fine: Decide a revalidate window that matches the business need (seconds/minutes/hours).</p>
</li>
<li><p>React Query: set staleTime based on how often the data actually changes, and make query keys match the inputs.</p>
</li>
<li><p>APIs: set Cache-Control / Vary intentionally so shared caches don’t mix user-specific responses.</p>
</li>
</ul>
<h2 id="heading-recap">Recap</h2>
<p>Caching itself isn’t the problem. Stale UI happens when a cache exists but you didn’t choose it intentionally or align it with the data’s freshness requirements.</p>
<p>If the UI won’t update, it’s usually because you’re seeing a saved copy from React Query, Next.js, the browser, a CDN, or a service worker. And sometimes it’s not a cache at all, it’s local React state</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Get Type Safety Without Code Generation Using tRPC and Hono ]]>
                </title>
                <description>
                    <![CDATA[ Have you ever updated your backend API property name but neglected to also update the frontend? I'm sure you have. When this occurs, it leads to production crashes and unhappy customers, plus you've wasted your entire week fixing the problem. To reso... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/type-safety-without-code-generation-using-trpc-and-hono/</link>
                <guid isPermaLink="false">696535106ca3442fa7874d4b</guid>
                
                    <category>
                        <![CDATA[ trpc ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ full stack ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ hono ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Tarun Singh ]]>
                </dc:creator>
                <pubDate>Mon, 12 Jan 2026 17:53:20 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768240380001/79d5aa1f-438e-4a1e-a072-3166b9a36333.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Have you ever updated your backend API property name but neglected to also update the frontend? I'm sure you have. When this occurs, it leads to production crashes and unhappy customers, plus you've wasted your entire week fixing the problem.</p>
<p>To resolve this issue in the past, you typically had to create a multitude of TypeScript interfaces by hand, using GraphQL Code Generator to generate the interface files, or hope that it all worked out. Well, there’s a better way to accomplish this now, without the need for code generation.</p>
<p><a target="_blank" href="https://trpc.io/">tRPC</a> and <a target="_blank" href="https://hono.dev/">Hono</a> are two applications that are changing how we develop TypeScript-based applications throughout the entirety of the full-stack.</p>
<p>By the end of this tutorial, you’ll understand:</p>
<ul>
<li><p>Why traditional REST APIs fail at type safety</p>
</li>
<li><p>How tRPC provides full end-to-end type inference between backend and frontend</p>
</li>
<li><p>How Hono delivers type-safe APIs while staying REST-friendly</p>
</li>
<li><p>When to choose tRPC vs Hono for your projects</p>
</li>
<li><p>How these tools improve developer experience, team velocity, and reliability</p>
</li>
</ul>
<p>If you’re building full-stack TypeScript applications and want fewer runtime bugs and faster iteration, this guide is for you.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-problem-with-traditional-apis">The Problem with Traditional APIs</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-makes-trpc-different">What Makes tRPC Different?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-hono-the-lightweight-challenger">Hono: The Lightweight Challenger</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-this-matters-these-days">Why This Matters These Days</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-getting-started">Getting Started</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-future-is-type-safe">The Future is Type-Safe</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along comfortably, you should have:</p>
<ul>
<li><p>Basic knowledge of TypeScript</p>
</li>
<li><p>Familiarity with REST APIs and how frontend-backend communication works</p>
</li>
<li><p>Some experience with Node.js and modern JavaScript frameworks</p>
</li>
<li><p>A general understanding of frontend frameworks like React or Next.js (helpful, but not required)</p>
</li>
</ul>
<p>You don’t need prior experience with tRPC, Hono, or GraphQL.</p>
<h2 id="heading-the-problem-with-traditional-apis">The Problem with Traditional APIs</h2>
<p>You’ve probably written something like this a hundred times:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Backend (Express)</span>
app.post(<span class="hljs-string">'/api/users'</span>, <span class="hljs-function">(<span class="hljs-params">req, res</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> { name, email } = req.body;
  <span class="hljs-comment">// Do stuff with user</span>
  res.json({ id: <span class="hljs-number">1</span>, name, email });
});

<span class="hljs-comment">// Frontend</span>
<span class="hljs-keyword">const</span> createUser = <span class="hljs-keyword">async</span> (name: <span class="hljs-built_in">string</span>, email: <span class="hljs-built_in">string</span>) =&gt; {
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'/api/users'</span>, {
    method: <span class="hljs-string">'POST'</span>,
    headers: { <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span> },
    body: <span class="hljs-built_in">JSON</span>.stringify({ name, email })
  });
  <span class="hljs-keyword">return</span> response.json(); 
};
</code></pre>
<p>The backend knows the shape of the data. But the frontend...it hopes it gets it right. You end up writing interfaces manually, like:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">interface</span> User {
  id: <span class="hljs-built_in">number</span>;
  name: <span class="hljs-built_in">string</span>;
  email: <span class="hljs-built_in">string</span>;
}
</code></pre>
<p>If you change the backend tomorrow to return <code>userId</code> instead of <code>id</code>, TypeScript won't catch it. Your types and reality have diverged, and you won't know until runtime.</p>
<p><a target="_blank" href="https://graphql.org/">GraphQL</a> tried to solve this with schemas and codegen, but honestly? Setting up GraphQL feels like assembling IKEA furniture without instructions. You need a schema, resolvers, code generation tools, and suddenly your "simple" API has a 30-minute setup process.</p>
<h2 id="heading-what-makes-trpc-different">What Makes tRPC Different?</h2>
<p>tRPC flips the script entirely. Instead of defining your API in a separate schema language, your TypeScript code is the schema. Here's the same API in tRPC:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Backend (tRPC router)</span>
<span class="hljs-keyword">import</span> { initTRPC } <span class="hljs-keyword">from</span> <span class="hljs-string">'@trpc/server'</span>;
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>;

<span class="hljs-keyword">const</span> t = initTRPC.create();

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> appRouter = t.router({
  createUser: t.procedure
    .input(z.object({
      name: z.string(),
      email: z.string().email(),
    }))
    .mutation(<span class="hljs-function">(<span class="hljs-params">{ input }</span>) =&gt;</span> {
      <span class="hljs-comment">// Do stuff with user</span>
      <span class="hljs-keyword">return</span> { id: <span class="hljs-number">1</span>, name: input.name, email: input.email };
    }),
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> AppRouter = <span class="hljs-keyword">typeof</span> appRouter;
</code></pre>
<p>This is where it gets cool. On your frontend:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Frontend - fully type-safe!</span>
<span class="hljs-keyword">import</span> { createTRPCClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@trpc/client'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { AppRouter } <span class="hljs-keyword">from</span> <span class="hljs-string">'./server'</span>;

<span class="hljs-keyword">const</span> client = createTRPCClient&lt;AppRouter&gt;({
  url: <span class="hljs-string">'http://localhost:3000/trpc'</span>,
});

<span class="hljs-comment">// TypeScript knows EVERYTHING about this call</span>
<span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> client.createUser.mutate({
  name: <span class="hljs-string">'Alice'</span>,
  email: <span class="hljs-string">'alice@example.com'</span>
});

<span class="hljs-comment">// user is automatically typed as { id: number; name: string; email: string; }</span>
</code></pre>
<p>No code generation or build step, or GraphQL schema. Just pure TypeScript inference doing its thing. If you rename <code>id</code> to <code>userId</code> in your backend, your frontend will immediately show a TypeScript error. You'll catch it before you even save the file.</p>
<p>This is what we call end-to-end type safety, and it's honestly a great transition.</p>
<h2 id="heading-hono-the-lightweight-challenger">Hono: The Lightweight Challenger</h2>
<p>While tRPC is amazing for full-stack TypeScript apps where you control both ends, <a target="_blank" href="https://hono.dev/">Hono</a> takes a slightly different approach. It's a lightweight web framework that gives you type safety while still being a traditional HTTP framework.</p>
<p>Here's the same example in Hono:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Hono } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono'</span>;
<span class="hljs-keyword">import</span> { z } <span class="hljs-keyword">from</span> <span class="hljs-string">'zod'</span>;
<span class="hljs-keyword">import</span> { zValidator } <span class="hljs-keyword">from</span> <span class="hljs-string">'@hono/zod-validator'</span>;

<span class="hljs-keyword">const</span> app = <span class="hljs-keyword">new</span> Hono();

<span class="hljs-keyword">const</span> userSchema = z.object({
  name: z.string(),
  email: z.string().email(),
});

app.post(<span class="hljs-string">'/api/users'</span>, zValidator(<span class="hljs-string">'json'</span>, userSchema), <span class="hljs-function">(<span class="hljs-params">c</span>) =&gt;</span> {
  <span class="hljs-keyword">const</span> { name, email } = c.req.valid(<span class="hljs-string">'json'</span>);
  <span class="hljs-keyword">return</span> c.json({ id: <span class="hljs-number">1</span>, name, email });
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">type</span> AppType = <span class="hljs-keyword">typeof</span> app;
</code></pre>
<p>On the frontend, you can use Hono’s RPC client:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { hc } <span class="hljs-keyword">from</span> <span class="hljs-string">'hono/client'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { AppType } <span class="hljs-keyword">from</span> <span class="hljs-string">'./server'</span>;

<span class="hljs-keyword">const</span> client = hc&lt;AppType&gt;(<span class="hljs-string">'http://localhost:3000'</span>);

<span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> client.api.users.$post({
  json: { name: <span class="hljs-string">'Bob'</span>, email: <span class="hljs-string">'bob@example.com'</span> }
});

<span class="hljs-keyword">const</span> user = <span class="hljs-keyword">await</span> response.json();
<span class="hljs-comment">// user is fully typed!</span>
</code></pre>
<p>Hono is incredibly fast (it runs on Cloudflare Workers, Deno, Bun, and Node.js), and it gives you that sweet type safety while still being a "regular" HTTP framework. You get RESTful routes, middleware, and all the familiar patterns – just with TypeScript powers.</p>
<h2 id="heading-why-this-matters-these-days">Why This Matters These Days</h2>
<p>You might think to yourself, “Okay, I know what you mean, but why should I care about it?” There’s a reason why these tools are being utilized more now than ever before.</p>
<h3 id="heading-developer-experience-is-essential">Developer experience is essential</h3>
<p>In 2026 and beyond, we’ll no longer accept long feedback loops. The ability to modify your backend code and see what might break on your frontend application without having to run the application will be fantastic for productivity. We’ll spend less time fixing bugs and more time creating new functionalities.</p>
<h3 id="heading-smaller-teams-better-apps">Smaller teams, better apps</h3>
<p>With tRPC or Hono, one developer can create an entire full-stack application with type safety at a very fast pace because they don’t have to switch back and forth between REST documentation and TypeScript interfaces – all the data is flowing to and from their backend code directly to their frontend.</p>
<h3 id="heading-the-end-of-works-on-my-machine">The end of “Works on my machine“</h3>
<p>With type safety, errors are caught at compile time instead of at the time your end-user clicks on a button. This is especially impactful when working in larger teams, when the backend developers and front-end developers may not be in constant communication with one another.</p>
<h2 id="heading-getting-started">Getting Started</h2>
<p>Want to try this out? Here's the fastest way:</p>
<p>For tRPC:</p>
<pre><code class="lang-bash">npm create @trpc/next-app@latest
</code></pre>
<p>This scaffolds a Next.js app with tRPC already configured. Check out the <a target="_blank" href="https://trpc.io/docs/client/nextjs">official tRPC docs</a> for more.</p>
<p>For Hono:</p>
<pre><code class="lang-bash">npm create hono@latest
</code></pre>
<p>Pick your runtime (Node.js, Cloudflare Workers, etc.), and you're off to the races. The <a target="_blank" href="https://hono.dev/">Hono documentation</a> is excellent and super approachable.</p>
<h2 id="heading-the-future-is-type-safe">The Future is Type-Safe</h2>
<p>Look, REST isn't going anywhere, and GraphQL has its place. But for full-stack TypeScript developers, tRPC and Hono represent something special: type safety without the ceremony. No code generation or no schema duplication, just TypeScript doing what it does best.</p>
<p>In the future, when you start a new project, give one of these a shot. Your future self – the one who's refactoring code at 2 AM – will thank you.</p>
<p>Happy coding!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build Your First Shopify App: A Beginner’s Guide ]]>
                </title>
                <description>
                    <![CDATA[ Shopify powers more than a million online stores around the world.  Many store features you see every day, such as discounts, bundles, and order fulfillment are built using apps. These apps are created by developers to extend Shopify and solve real p... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-your-first-shopify-app-a-beginners-guide/</link>
                <guid isPermaLink="false">696023bf8de30d6097e30e99</guid>
                
                    <category>
                        <![CDATA[ shopify ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ecommerce ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Thu, 08 Jan 2026 21:38:07 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767908229340/434160c9-891e-46d9-82fe-905d1b5ef2cb.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p><a target="_blank" href="http://shopify.com/">Shopify</a> powers more than a million online stores around the world. </p>
<p>Many store features you see every day, such as discounts, bundles, and order fulfillment are built using apps. These apps are created by developers to extend Shopify and solve real problems for merchants. </p>
<p>If you know JavaScript and basic web development, you already have enough skills to start building Shopify apps.</p>
<p>In this tutorial, you’ll learn what a Shopify app is, how Shopify apps work, and how to set up your development environment. You’ll also see three real examples of popular Shopify apps and how they are built. </p>
<h2 id="heading-what-well-cover">What We’ll Cover</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-a-shopify-app">What Is a Shopify App?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-shopify-apps-work">How Shopify Apps Work</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-setting-up-your-development-environment">Setting Up Your Development Environment</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-understanding-shopify-apis">Understanding Shopify APIs</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-case-study-one-bundle-and-discount-apps">Case Study One: Bundle and Discount Apps</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-case-study-two-printful-and-order-fulfilment">Case Study Two: Printful and Order Fulfilment</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-case-study-three-shiprocket-and-shipping-rates">Case Study Three: Shiprocket and Shipping Rates</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-testing-your-shopify-app">Testing Your Shopify App</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-preparing-for-the-shopify-app-store">Preparing for the Shopify App Store</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<p>To follow this tutorial, you should be comfortable with JavaScript and APIs. Some experience with Node.js and npm will help, but you do not need to be an expert. No prior experience with Shopify is required.</p>
<h2 id="heading-what-is-a-shopify-app">What Is a Shopify App?</h2>
<p>A <a target="_blank" href="https://apps.shopify.com/">Shopify app</a> is a web application that connects to a Shopify store. The app runs on your own server or a serverless platform. </p>
<p>It talks to Shopify using <a target="_blank" href="https://shopify.dev/docs/api">secure APIs</a>. When a merchant installs your app, they allow it to access certain store data. This could include products, orders, or customers, depending on the permissions given.</p>
<p>There are different types of Shopify apps. </p>
<p>Public apps are listed on the Shopify App Store and can be installed by any merchant. These apps must pass a review before approval. </p>
<p>Custom apps are built for a single store, and private apps are used only inside a company. </p>
<p>Most Shopify apps include backend code that calls Shopify APIs, a frontend interface shown inside the Shopify Admin, storefront features that shoppers can see, and webhooks that react to store events.</p>
<h2 id="heading-how-shopify-apps-work">How Shopify Apps Work</h2>
<p>When a merchant installs your app, Shopify starts an <a target="_blank" href="https://auth0.com/intro-to-iam/what-is-oauth-2">OAuth process</a>. This is a secure way to ask the merchant for permission. </p>
<p>Once approved, Shopify sends your app an access token. This token allows your app to make API calls to the store.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767768422978/61189891-7a02-449a-9da7-30c6d1116638.png" alt="Oauth flow" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Shopify apps can add screens inside the admin area using <a target="_blank" href="https://shopify.dev/docs/api/app-bridge">App Bridge</a> and Polaris. These tools make your app feel like part of Shopify. Apps can also add features to the storefront using theme app extensions. </p>
<p>Shopify provides both REST and GraphQL APIs. REST is easy to use, but GraphQL is faster and more efficient. Today, most new apps use GraphQL.</p>
<h2 id="heading-setting-up-your-development-environment">Setting Up Your Development Environment</h2>
<p>Before you start coding, you’ll need a few tools. You’ll need to install Node.js and the <a target="_blank" href="https://shopify.dev/docs/api/shopify-cli">Shopify CLI</a>. You’ll also need a <a target="_blank" href="https://www.shopify.com/in/partners">Shopify Partner account</a>. The Partner account lets you create apps and test them without cost.</p>
<p>The Shopify CLI helps you create a starter app quickly. You can generate a working app by running these commands:</p>
<pre><code class="lang-python">shopify app create node
cd my-shopify-app
npm install
shopify app serve
</code></pre>
<p>This creates an app with login, authentication, and an embedded admin interface. It also sets up a secure tunnel so Shopify can reach your local server while you develop.</p>
<h2 id="heading-understanding-shopify-apis">Understanding Shopify APIs</h2>
<p>Shopify provides APIs for almost every part of a store. This includes products, orders, customers, and shipping. Your app can only access data that the merchant has allowed during installation.</p>
<p>Here’s a simple example of fetching products using the GraphQL Admin API:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> fetch <span class="hljs-keyword">from</span> <span class="hljs-string">"node-fetch"</span>;

<span class="hljs-keyword">async</span> function getProducts(shop, token) {
  const query = `
  {
    products(first: <span class="hljs-number">5</span>) {
      edges {
        node {
          id
          title
        }
      }
    }
  }
  `;
  const response = <span class="hljs-keyword">await</span> fetch(
    `https://${shop}/admin/api/<span class="hljs-number">2025</span><span class="hljs-number">-10</span>/graphql.json`,
    {
      method: <span class="hljs-string">"POST"</span>,
      headers: {
        <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
        <span class="hljs-string">"X-Shopify-Access-Token"</span>: token
      },
      body: JSON.stringify({ query })
    }
  );
  const data = <span class="hljs-keyword">await</span> response.json();
  <span class="hljs-keyword">return</span> data.data.products.edges;
}
</code></pre>
<p>This function gets the first five products from a store. The access token comes from the OAuth process during app installation.</p>
<h2 id="heading-case-study-one-bundle-and-discount-apps">Case Study One: Bundle and Discount Apps</h2>
<p>Bundle and discount apps help merchants offer deals like “Buy two, get ten percent off.” These apps must work with Shopify’s pricing rules and checkout system. A popular example is the <a target="_blank" href="https://apps.shopify.com/bundle-deals">Bundle Deals app</a>, which shows offers on product and cart pages.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767768457881/f98bd8fc-7659-4841-9dfe-8adb2aa6f191.png" alt="Bundle deals app" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>These apps usually add small UI elements to the storefront using theme app extensions. They use Shopify’s Discount APIs to apply offers safely. </p>
<p>They don’t change checkout directly. Instead, they enhance the storefront and let Shopify handle the final pricing.</p>
<p>A storefront script might look like this:</p>
<pre><code class="lang-python">fetch(<span class="hljs-string">"/apps/bundle-deals/api/bundles?productId=gid://shopify/Product/123"</span>)
  .then((res) =&gt; res.json())
  .then((bundles) =&gt; {
    displayBundles(bundles);
  });
</code></pre>
<p>This code runs in the store’s frontend. It fetches bundle rules from your app server and shows them to shoppers.</p>
<h2 id="heading-case-study-two-printful-and-order-fulfilment">Case Study Two: Printful and Order Fulfilment</h2>
<p><a target="_blank" href="https://apps.shopify.com/printful">Printful</a> is a popular app that connects Shopify stores with a print-on-demand service (for example, T-shirts, Mugs, and so on). When a customer places an order, Printful receives the order and starts production.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767768478088/0f225294-ce0a-429c-a131-d263db439beb.png" alt="Printful app" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Apps like this need access to orders and reliable event handling. They use webhooks to listen for new orders. When Shopify sends a webhook, the app forwards the data to an external system.</p>
<p>Here is a simple webhook example:</p>
<pre><code class="lang-python">app.post(<span class="hljs-string">"/webhooks/orders/create"</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  const order = req.body;

<span class="hljs-keyword">await</span> printfulClient.createOrder({
    external_id: order.id,
    items: order.line_items.map(item =&gt; ({
      variant_id: item.variant_id,
      quantity: item.quantity
    }))
  });
  res.status(<span class="hljs-number">200</span>).send(<span class="hljs-string">"Order processed"</span>);
});
</code></pre>
<p>This keeps Shopify and Printful in sync. The same pattern is used for shipping tools, accounting software, and CRMs.</p>
<h2 id="heading-case-study-three-shiprocket-and-shipping-rates">Case Study Three: Shiprocket and Shipping Rates</h2>
<p><a target="_blank" href="https://apps.shopify.com/shiprocket">Shiprocket</a> helps merchants manage shipping and delivery. Shipping apps often calculate rates in real time and update order status after shipment.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1767768510499/2e5351a1-c006-4ac2-b373-47acf63189a3.jpeg" alt="Shiprocket app" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Since Shopify restricts what can run during checkout, shipping apps typically calculate rates before checkout begins or use carrier service APIs. A simple rate endpoint might look like this:</p>
<pre><code class="lang-python">app.post(<span class="hljs-string">"/shipping/rates"</span>, <span class="hljs-keyword">async</span> (req, res) =&gt; {
  const { destination, items } = req.body;
  const rates = <span class="hljs-keyword">await</span> fetchCarrierRates(destination, items);

res.json({
    rates: rates.map(rate =&gt; ({
      service_name: rate.name,
      total_price: rate.price
    }))
  });
});
</code></pre>
<p>This lets merchants show shipping options to customers before they reach checkout.</p>
<h2 id="heading-testing-your-shopify-app">Testing Your Shopify App</h2>
<p>Testing is a critical part of building a reliable Shopify app, especially if you plan to submit it to the App Store. Every feature should be tested thoroughly in a development store before you consider a release. A development store lets you simulate real merchant behavior without affecting live data, which makes it ideal for both manual and automated testing.</p>
<p>In addition to live testing, Shopify allows you to <a target="_blank" href="https://mock.shop/">mock API responses</a> during development. Mocking lets you test your business logic without relying on real API calls or rate limits. This is especially useful when simulating error scenarios or incomplete data, such as missing fields or unexpected values.</p>
<p>By combining real store testing with mocked responses, you can be confident that your app behaves correctly in both normal and failure conditions.</p>
<h2 id="heading-preparing-for-the-shopify-app-store">Preparing for the Shopify App Store</h2>
<p>Preparing for the Shopify App Store is an important step if you want to release a public app that merchants can trust and install with confidence. Shopify has a <a target="_blank" href="https://shopify.dev/docs/apps/launch/app-store-review/review-process">formal review process</a>, and your app must meet both technical and policy requirements before it can be listed.</p>
<p>Your app should request only the API permissions that are absolutely necessary for its core functionality. Asking for extra or unrelated permissions is one of the most common reasons apps get rejected. Shopify expects you to clearly explain why each permission is needed and how the data will be used. This helps merchants feel safe when installing your app.</p>
<p>You must also include basic legal and support information. This includes a clear privacy policy that explains what data you collect and how you handle it, terms of service that define usage rules, and a visible support contact such as an email address or help page.</p>
<p>Finally, Shopify looks closely at app quality. Your app should be stable, handle errors gracefully, and be tested across common store scenarios. Clear messaging, predictable behavior, and transparent data usage go a long way in passing the review process.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Building your first Shopify app takes time, but it’s very achievable. Start with login and API calls. Learn how to embed UI inside Shopify. Study real apps from the Shopify app store. Each one solves a different problem using a different design.</p>
<p>As you practice, the pieces will start to fit together. Keep building, testing, and reading the documentation. Your first Shopify app could be the start of something much bigger.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ MCP vs APIs: What's the Real Difference? ]]>
                </title>
                <description>
                    <![CDATA[ APIs and MCPs both help systems talk to each other.  At first, they might look the same. Both allow one piece of software to ask another for data or perform an action. But the way they work and the reason they exist are completely different. An API, ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/mcp-vs-apis-whats-the-real-difference/</link>
                <guid isPermaLink="false">69028c7be37f0cfba4d00775</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mcp ]]>
                    </category>
                
                    <category>
                        <![CDATA[ llm ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Wed, 29 Oct 2025 21:51:55 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1761774679622/477d1991-a083-4ae6-8e3b-2a186d254274.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>APIs and MCPs both help systems talk to each other. </p>
<p>At first, they might look the same. Both allow one piece of software to ask another for data or perform an action. But the way they work and the reason they exist are completely different.</p>
<p>An API, or <a target="_blank" href="https://www.ibm.com/think/topics/api">Application Programming Interface</a>, is built for developers. It’s how one program communicates with another. MCP, or <a target="_blank" href="https://modelcontextprotocol.io/">Model Context Protocol</a>, is built for AI models. It’s how a large language model like GPT or Claude can safely talk to external systems and use tools.</p>
<p>Let’s look at what makes them different, why MCP exists when APIs already do the job, and how they work in real examples.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-an-api">What is an API</a>?</p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-mcp">What is MCP</a>?</p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-mcp-works">How MCP Works</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-not-just-use-an-api">Why Not Just Use an API</a>?</p>
</li>
<li><p><a class="post-section-overview" href="#heading-mcp-vs-api-in-practice">MCP vs API in Practice</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-key-conceptual-difference">Key Conceptual Difference</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-discovery-and-schema">Discovery and Schema</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-security-and-privacy">Security and Privacy</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-the-future-of-mcp">The Future of MCP</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-an-api">What is an API?</h2>
<p>An API is a set of rules that lets software talk to software. </p>
<p>It is like a waiter in a restaurant. You tell the waiter what you want, the kitchen prepares it, and the waiter brings it back. You never go into the kitchen yourself.</p>
<p>For example, if you want to get details of a GitHub user, you can make a simple API request.</p>
<pre><code class="lang-plaintext">GET https://api.github.com/users/username
</code></pre>
<p>The server replies with a response like this:</p>
<pre><code class="lang-plaintext">{
  "login": "john",
  "id": 12345,
  "followers": 120,
  "repos": 42
}
</code></pre>
<p>The API follows a pattern that both the client and the server understand. Developers use APIs every day to connect systems like payment gateways, weather data, or user accounts.</p>
<p>APIs are built for humans to code against. A developer writes the logic, sends requests, handles errors, adds authentication, and decides what to do with the response.</p>
<h2 id="heading-what-is-mcp">What is MCP?</h2>
<p>MCP stands for <a target="_blank" href="https://www.turingtalks.ai/p/how-model-context-protocol-works">Model Context Protocol</a>. It’s a new standard that allows AI models to interact with external tools, data, and systems in a safe, structured way.</p>
<p>MCP is not meant for developers directly. It’s meant for large language models. </p>
<p>An AI model cannot make network requests by itself. It doesn’t know how to use headers, tokens, or API formats. It just predicts text based on what you type.</p>
<p>So if you tell a model, “Get the weather for Delhi,” it might generate some text that looks like a Python request. But it cannot actually run that code.</p>
<p>That is where MCP comes in. MCP acts like a bridge between the AI model and the real world. It defines a set of “tools” that the model can use safely. </p>
<p>Each tool is described using a schema so that the model knows what the tool does, what inputs it takes, and what it returns.</p>
<h2 id="heading-how-mcp-works">How MCP Works</h2>
<p>You can think of MCP as a server that runs in the background. It exposes tools that an AI model can call. Each tool is a small piece of code that performs an action.</p>
<p>For example, you can write a simple MCP server in Python like this:</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> mcp.server.fastmcp <span class="hljs-keyword">import</span> FastMCP
<span class="hljs-keyword">import</span> requests

mcp = FastMCP(name=<span class="hljs-string">"github-tools"</span>)
<span class="hljs-meta">@mcp.tool()</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_repos</span>(<span class="hljs-params">username: str</span>):</span>
    <span class="hljs-string">"""Fetch public repositories for a user"""</span>
    url = <span class="hljs-string">f"https://api.github.com/users/<span class="hljs-subst">{username}</span>/repos"</span>
    <span class="hljs-keyword">return</span> requests.get(url).json()
mcp.run()
</code></pre>
<p>This server defines a single tool called get_repos. It takes a username and fetches their GitHub repositories using the GitHub API.</p>
<p>Now, if an AI model is connected to this MCP server, it can ask for “get_repos for user john” and receive the data. The model does not know or care about the actual URL, headers, or tokens. The MCP server handles that part.</p>
<h2 id="heading-why-not-just-use-an-api">Why Not Just Use an API?</h2>
<p>You might wonder, why not just let the AI model call the API directly? If the model can talk to APIs, why add another layer?</p>
<p>The short answer is that AI models cannot safely call APIs on their own. They have no built-in execution environment, no way to store secrets, and no limits. </p>
<p>Letting a model make arbitrary network requests would be dangerous. It could expose keys, access private data, or even cause damage by mistake.</p>
<p>MCP solves that problem by creating a controlled layer between the model and your systems. You decide which tools the model can use. You can restrict inputs, filter outputs, and monitor everything the model does.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1761561683902/6b0f2299-041e-4ef4-a6ce-726899c52fbf.png" alt="MCP Architecture" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>In an MCP setup, the model never sees API keys or sensitive URLs. It just calls a tool that you define. The tool itself handles the network call and returns only the safe data.</p>
<p>This makes MCP much safer for real-world use, especially in enterprise or private environments.</p>
<h2 id="heading-mcp-vs-api-in-practice">MCP vs API in Practice</h2>
<p>Let’s take a simple example. Suppose you want an AI to fetch weather data.</p>
<p>If you were using an API, you might write code like this:</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> requests
response = requests.get(<span class="hljs-string">"https://api.weatherapi.com/v1/current.json?key=API_KEY&amp;q=Delhi"</span>)
print(response.json())
</code></pre>
<p>That works fine if a human developer runs it. But if an AI model tried to do the same, it would need access to your API key, network, and code execution. That is unsafe.</p>
<p>With MCP, you can define a tool like this:</p>
<pre><code class="lang-python"><span class="hljs-meta">@mcp.tool()</span>
<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">get_weather</span>(<span class="hljs-params">city: str</span>):</span>
    <span class="hljs-string">"""Get weather for a city"""</span>
    <span class="hljs-keyword">import</span> requests
    url = <span class="hljs-string">f"https://api.weatherapi.com/v1/current.json?key=API_KEY&amp;q=<span class="hljs-subst">{city}</span>"</span>
    <span class="hljs-keyword">return</span> requests.get(url).json()
</code></pre>
<p>Now the AI model can simply say, “Call get_weather with city=Delhi,” and the MCP server runs the function.</p>
<p>The model does not see the API key or the actual URL. It just uses the tool safely.</p>
<h2 id="heading-key-conceptual-difference">Key Conceptual Difference</h2>
<p>The difference between MCP and API is not just technical. It’s also philosophical.</p>
<p>APIs are for humans to use directly. They assume the caller understands the system, can handle tokens, and knows how to format requests.</p>
<p>MCP is for AI models. It assumes the caller is an intelligent but untrusted system that cannot hold secrets or execute code. The protocol gives the model only what it needs to perform reasoning and tool usage.</p>
<p>So while APIs expose endpoints like <code>/users</code> or <code>/weather</code>, MCP exposes capabilities like “get_user_info” or “get_weather.” The AI model does not call URLs. It calls functions with typed parameters.</p>
<h2 id="heading-discovery-and-schema">Discovery and Schema</h2>
<p>Another big advantage of MCP is that it can tell the model what tools are available.</p>
<p>When an AI model connects to an MCP server, it can ask for a list of tools. The server replies with their names, descriptions, and parameters in a structured format.</p>
<p>For example, the model might receive something like this:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"tools"</span>: [
    {
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">"get_weather"</span>,
      <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Get weather for a city"</span>,
      <span class="hljs-attr">"parameters"</span>: {
        <span class="hljs-attr">"city"</span>: {<span class="hljs-attr">"type"</span>: <span class="hljs-string">"string"</span>}
      }
    }
  ]
}
</code></pre>
<p>This means the model does not need separate documentation or prompt tuning. It knows exactly how to call each tool.</p>
<p>In contrast, an API would require reading human-written docs, copying sample requests, and guessing formats.</p>
<h2 id="heading-security-and-privacy">Security and Privacy</h2>
<p>MCP provides better control over what the model can do.</p>
<p>Since the tools are defined in your server, you can add rules, limits, and validations. You can prevent the model from sending dangerous inputs or accessing private data.</p>
<p>For example, your tool can reject requests that ask for too much data or contain suspicious patterns. You can also log every call for audit purposes.</p>
<p>APIs, on the other hand, are exposed over the internet. If an API key leaks or a model calls the wrong endpoint, you could face a data breach.</p>
<p>With MCP, everything can run locally, behind a firewall, or on a private network. The model never needs direct access to the outside world.</p>
<h2 id="heading-the-future-of-mcp">The Future of MCP</h2>
<p>Big AI companies like OpenAI and Anthropic are adopting MCP as a shared standard. That means any model that supports MCP can use your tools without modification.</p>
<p>If you build a weather MCP server today, it could work with GPT, Claude, or any other MCP-compatible model in the future.</p>
<p>This makes MCP a unifying layer between AI systems and external tools, much like APIs are for web applications.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>At a glance, MCP and APIs might seem similar because both pass data between systems. But the difference lies in for whom they are built.</p>
<p>APIs are built for developers and systems that can safely make network calls. MCP is built for AI models that reason with text but cannot safely execute code.</p>
<p>An API gives you endpoints to access data. MCP gives the AI tools to use that data safely.</p>
<p>Think of it this way. APIs connect machines. MCP connects intelligence to machines.</p>
<p>That is why MCP is not replacing APIs but sitting above them as a new layer. APIs will still provide the data. MCP will just make it possible for AI to use those APIs safely, with structure, control, and understanding.</p>
<p><em>Hope you enjoyed this article. Signup for my free AI newsletter</em> <a target="_blank" href="https://www.turingtalks.ai/"><strong><em>TuringTalks.ai</em></strong></a> <em>for more hands-on tutorials on AI. You can also find</em> <a target="_blank" href="https://manishshivanandhan.com/"><strong><em>visit my website</em></strong></a><em>.</em></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Turn Websites into LLM-Ready Data Using Firecrawl ]]>
                </title>
                <description>
                    <![CDATA[ If you’ve ever tried feeding web pages into an AI model, you know the pain. Websites come with ads, navigation bars, and messy HTML. Before your Large Language Model (LLM) can understand the content, you must clean and format it. That’s where Firecra... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-turn-websites-into-llm-ready-data-using-firecrawl/</link>
                <guid isPermaLink="false">68f9002b255bb54c291b6f88</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ web scraping ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ llm ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Wed, 22 Oct 2025 16:02:51 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1761148818578/a9572dc3-cc79-4ba9-ab47-4270e465df70.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you’ve ever tried feeding web pages into an AI model, you know the pain.</p>
<p>Websites come with ads, navigation bars, and messy HTML. Before your Large Language Model (LLM) can understand the content, you must clean and format it.</p>
<p>That’s where <a target="_blank" href="https://github.com/firecrawl/firecrawl">Firecrawl</a> makes life easy. It’s an open-source API tool that turns any website into neat, structured data ready for LLMs in seconds.</p>
<p>In this tutorial, we’ll look at two ways of using Firecrawl. One is through Firecrawl’s API (a paid API with a free tier) and the other is a self-hosted version.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-firecrawl">What Is Firecrawl?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-llms-need-clean-data">Why LLMs Need Clean Data</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-setting-up-firecrawl">Setting Up Firecrawl</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-scraping-a-single-page">Scraping a Single Page</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-crawling-an-entire-website">Crawling an Entire Website</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-extracting-structured-data-with-ai">Extracting Structured Data with AI</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-self-hosting-firecrawl-using-sevalla">Self-hosting Firecrawl using Sevalla</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-use-cases">Use Cases</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-firecrawl">What Is Firecrawl?</h2>
<p><a target="_blank" href="https://www.firecrawl.dev/">Firecrawl</a> is a web crawling and scraping service that helps developers collect clean data from websites. You give it a URL, and it returns the content in formats like Markdown, HTML, JSON, or even screenshots.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760534000207/93e57884-c611-40cc-b7be-4fe7d3c1ac5c.png" alt="Firecrawl illustrated - open source and cloud version" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Unlike basic scrapers, Firecrawl understands complex websites that load content with JavaScript. It can crawl through links, follow pages, and handle the heavy lifting like proxies and anti-bot systems automatically.</p>
<p>In short, it does the hard part of web data collection, so you can focus on using that data for your AI or automation projects.</p>
<h2 id="heading-why-llms-need-clean-data">Why LLMs Need Clean Data</h2>
<p>LLMs learn and respond based on the text you give them. If that text includes clutter like HTML tags, scripts, or irrelevant sections, the AI gets confused.</p>
<p>Clean, well-structured data helps the model stay focused on the real content, like the article body, product details, or documentation.</p>
<p>Firecrawl makes this process simple. Instead of spending hours building scrapers or cleaning text, you can get ready-to-use content in a single API call.</p>
<h2 id="heading-setting-up-firecrawl">Setting Up Firecrawl</h2>
<p>To get started, create an account on <a target="_blank" href="https://firecrawl.dev/">firecrawl.dev</a> and grab your API key. Running Firecrawl on your machine includes setting up a server, Redis cache, and so on. So we’ll use the API key from firecrawl.dev to test the API.</p>
<p>We can also quickly test its capabilities in the UI of the website.</p>
<p>Let’s use <a target="_blank" href="https://freecodecamp.org/">https://freecodecamp.org</a> as the domain to see if Firecrawl can return some results.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760534104974/eb7ebdb4-d91f-4c49-92d9-b1c56c86ebb1.png" alt="Crawling freeCodeCamp" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>And yes, we can see several URLs scraped by Firecrawl.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760534508920/5f6423e6-aef2-4935-a821-0246fd96c12e.png" alt="Firecrawl results" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Now let’s access Firecrawl using code. The free plan lets you scrape 500 pages, so its all we need to understand how it works.</p>
<p>You can use either the <a target="_blank" href="https://docs.firecrawl.dev/sdks/python">Python SDK</a>, the <a target="_blank" href="https://docs.firecrawl.dev/sdks/node">Node.js SDK</a>, or direct API requests with curl.</p>
<p>Here’s how you install the SDKs:</p>
<p>Python:</p>
<pre><code class="lang-plaintext">pip install firecrawl-py
</code></pre>
<p>Node.js:</p>
<pre><code class="lang-plaintext">npm install @mendable/firecrawl-js
</code></pre>
<p>Once installed, you just need to set your API key and you’re ready to crawl.</p>
<h2 id="heading-scraping-a-single-page">Scraping a Single Page</h2>
<p>Let’s say you want to extract the main content from Firecrawl’s homepage. You can do this in just a few lines.</p>
<p><strong>Python Example:</strong></p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> firecrawl <span class="hljs-keyword">import</span> Firecrawl
</code></pre>
<pre><code class="lang-python">firecrawl = Firecrawl(api_key=<span class="hljs-string">"fc-YOUR_API_KEY"</span>)
</code></pre>
<pre><code class="lang-python">doc = firecrawl.scrape(
    <span class="hljs-string">"https://firecrawl.dev"</span>,
    formats=[<span class="hljs-string">"markdown"</span>, <span class="hljs-string">"html"</span>]
)
</code></pre>
<pre><code class="lang-python">print(doc.markdown)
</code></pre>
<p>This script returns the cleaned version of the page in Markdown format, perfect for an LLM to read or analyze.</p>
<p>With this one command, you get the core text, free from HTML clutter.</p>
<h2 id="heading-crawling-an-entire-website">Crawling an Entire Website</h2>
<p>If you need data from multiple pages like a full documentation site, you can crawl the entire domain. Firecrawl finds all the links and scrapes them automatically.</p>
<p>Example API call:</p>
<pre><code class="lang-plaintext">curl -X POST https://api.firecrawl.dev/v2/crawl \
  -H 'Authorization: Bearer fc-YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "url": "https://docs.firecrawl.dev",
    "limit": 10,
    "scrapeOptions": {
      "formats": ["markdown", "html"]
    }
  }'
</code></pre>
<p>This starts a crawl job and returns a job ID. Once done, you can download all the scraped pages in clean, LLM-ready formats.</p>
<h2 id="heading-extracting-structured-data-with-ai">Extracting Structured Data with AI</h2>
<p>One of Firecrawl’s best features is AI-powered extraction. You can ask Firecrawl to read a page and return structured data, like a product’s price, description, or reviews, in JSON format.</p>
<p>Example:</p>
<pre><code class="lang-plaintext">curl -X POST https://api.firecrawl.dev/v2/extract \
  -H 'Authorization: Bearer fc-YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "urls": ["https://firecrawl.dev/*"],
    "prompt": "Extract the company mission and whether it is open source.",
    "schema": {
      "type": "object",
      "properties": {
        "company_mission": { "type": "string" },
        "is_open_source": { "type": "boolean" }
      }
    }
  }'
</code></pre>
<p>Firecrawl uses a built-in LLM to read the content and fill in the structure automatically. You can even skip the schema and just provide a natural-language prompt, like:</p>
<blockquote>
<p><em>“Extract all the pricing details and feature names from this page.”</em></p>
</blockquote>
<p>This is ideal for AI pipelines, RAG (Retrieval-Augmented Generation) systems, or dashboards that rely on clean, structured data.</p>
<h2 id="heading-self-hosting-firecrawl-using-sevalla">Self-hosting Firecrawl using Sevalla</h2>
<p>Firecrawl is open source, which means you don’t have to pay for the API if you prefer full control. You can deploy it on your own server and customise it however you like.</p>
<p>You can install Firecrawl on your local machine by setting up a database, cache, and other required components. But this setup will only work for local projects and won’t allow you to build or deploy applications that use Firecrawl.</p>
<p>To install Firecrawl, you can choose any cloud provider like <a target="_blank" href="https://aws.amazon.com/">AWS</a>, <a target="_blank" href="https://www.heroku.com/">Heroku</a>, or others to setup this project. But I will be using Sevalla.</p>
<p><a target="_blank" href="https://sevalla.com/">Sevalla</a> is a modern, usage-based Platform-as-a-service provider. It offers application hosting, database, object storage, and static site hosting for your projects.</p>
<p>I am using Sevalla for hosting for two reasons:</p>
<ul>
<li><p>Every platform will charge you for creating a cloud resource. Sevalla comes with a $50 credit for us to use, so we won't incur any costs for this example.</p>
</li>
<li><p>Sevalla has a <a target="_blank" href="https://docs.sevalla.com/templates/overview">template for Firecrawl</a>, so it simplifies the manual installation and setup for each resource you will need for Firecrawl.</p>
</li>
</ul>
<p><a target="_blank" href="https://app.sevalla.com/login">Login</a> to Sevalla and click on Templates. You can see Firecrawl as one of the templates.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760534530797/6d327148-5c6f-40cc-863a-2ad9d82763bd.png" alt="Sevalla Templates" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Click “Deploy now” and choose a server in the pop-up, and click “Deploy”. Sevalla will start provisioning the resources we need for running our Firecrawl instance.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760534550817/38525217-89cb-41b3-8128-85164734b764.png" alt="Firecrawl resources" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Once the deployment is complete, you will see three instances provisioned:</p>
<ul>
<li><p>a <a target="_blank" href="https://redis.io/">Redis Cache</a></p>
</li>
<li><p>a server to run <a target="_blank" href="https://playwright.dev/">Playwright</a></p>
</li>
<li><p>The API application</p>
</li>
</ul>
<p>Go to the Firecrawl-API application. Under the deployments section, click on “Visit app” once the deployment is complete.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1760534571061/3f4db002-c775-445f-a1fc-af1aefff2d86.png" alt="Firecrawl Deployment" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>You can now use your private endpoint in your applications. My API URL is <a target="_blank" href="https://firecrawl-api-56t8x.sevalla.app/">https://firecrawl-api-56t8x.sevalla.app</a> (this is a temporary URL – dont use this), so I can replace api.firecrawl.dev with this URL.</p>
<pre><code class="lang-plaintext">curl -X POST https://firecrawl-api-56t8x.sevalla.app/v2/extract \
  -H 'Content-Type: application/json' \
  -d '{
    "urls": ["https://firecrawl.dev/*"],
    "prompt": "Extract the company mission and whether it is open source.",
    "schema": {
      "type": "object",
      "properties": {
        "company_mission": { "type": "string" },
        "is_open_source": { "type": "boolean" }
      }
    }
  }'
</code></pre>
<p>If you want to run the project locally by installing applications like Redis, Postgresql, and Playwright, <a target="_blank" href="https://github.com/firecrawl/firecrawl/blob/main/CONTRIBUTING.md">here’s a detailed guide</a>.</p>
<h2 id="heading-use-cases">Use Cases</h2>
<p>Developers and data scientists use Firecrawl for a wide range of tasks. They often rely on it to turn documentation sites into training data for large language models, ensuring that their models can learn from accurate and well-organised sources.</p>
<p>Others use it to collect blog posts or news articles for <a target="_blank" href="https://www.turingtalks.ai/p/how-to-build-a-simple-sentiment-analyzer-using-hugging-face-transformer">sentiment analysis</a>, helping them understand trends, opinions, or public reactions across the web.</p>
<p>Firecrawl is also valuable for monitoring web content changes, which is essential for research projects or compliance tracking where up-to-date information is critical.</p>
<p>Teams can also use it to build “chat with your website” AI assistants that can answer questions based on the latest site content.</p>
<p>In each of these cases, Firecrawl ensures that your model receives clean, structured, and consistent data, making it easier to build reliable and intelligent AI systems.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Turning messy websites into readable text used to be one of the toughest parts of building AI systems. Firecrawl changes that. With one API call, you can scrape, crawl, and extract high-quality data that your LLM can immediately understand.</p>
<p>If you’re building anything related to AI, RAG, or data pipelines, Firecrawl is one of those tools you’ll wish you had discovered earlier.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Securely Deploy APIs to Amazon Lambda – A Practical Guide ]]>
                </title>
                <description>
                    <![CDATA[ Cyber attacks against APIs (Application Programming Interfaces) are on the increase. These attacks arise from issues with proper authentication, authorization, unnecessary data exposure, lack of request limits, resource consumption, and use of vulner... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-securely-deploy-apis-to-amazon-lambda-a-practical-guide/</link>
                <guid isPermaLink="false">68e8418df4be6f5ede699317</guid>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ aws lambda ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ API Gateway ]]>
                    </category>
                
                    <category>
                        <![CDATA[ serverless ]]>
                    </category>
                
                    <category>
                        <![CDATA[ secrets management ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Agnes Olorundare ]]>
                </dc:creator>
                <pubDate>Thu, 09 Oct 2025 23:13:17 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1760051580641/75d09121-6167-4e06-94d7-53cf23a6f6a1.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Cyber attacks against APIs (Application Programming Interfaces) are on the increase. These attacks arise from issues with proper authentication, authorization, unnecessary data exposure, lack of request limits, resource consumption, and use of vulnerable third-party APIs.</p>
<p>Gaps in APIs can occur before requests reach the APIs, within the code housing the APIs, and even along the path of the APIs’ communication with downstream services, dependencies, or other microservices.</p>
<p>Attackers leverage flaws in APIs to gain access to confidential data, harvest or manipulate data, or even make your service unavailable through distributed denial of service attacks.</p>
<p>In this article, you’ll learn to deploy your APIs in Lambda and apply some security measures pre-function, within the function, and post-function.</p>
<h2 id="heading-table-of-contents"><strong>Table of Contents</strong></h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-an-api">What is an API?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-requirementsprerequisites">Requirements/Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-goal">Project Goal</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-overall-architecture">Project Overall Architecture</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-aws-set-up">AWS Set Up</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-clone-project">Clone Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-set-up-simple-notification-service">Set Up Simple Notification Service</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-set-up-secrets-manager">Set Up Secrets Manager</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-set-up-internal-lambda">Set Up Internal Lambda</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-set-up-external-lambda">Set Up External Lambda</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-configure-web-application-firewall">Configure Web Application Firewall</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-configure-cognito-user-pools">Configure Cognito User Pools</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-configure-api-gateway">Configure API Gateway</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-test-setup-end-to-end">Test Setup End-to-End</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-clean-up">Clean Up</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-improvements">Improvements</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-an-api">What is an API?</h2>
<p>The focus of this article is the security of Application Programming Interfaces (APIs). An API is an interface that connects two programms or applications, allowing them to exchange data and communicate.</p>
<p>An API can be internal to an organization or it can belong to a third-party that allows other users to consume their data through the API.</p>
<h2 id="heading-requirementsprerequisites">Requirements/Prerequisites</h2>
<p>While this tutorial is beginner-friendly, you’ll need the following prerequisites to follow along seamlessly:</p>
<ul>
<li><p>A basic knowledge of the AWS Cloud.</p>
</li>
<li><p>An AWS account with administrator access.</p>
</li>
<li><p>AWS CLI. You can find the installation guide <a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html">here</a>. Follow the instructions for your operating system.</p>
</li>
<li><p>Python. You can visit Python’s official documentation <a target="_blank" href="https://www.python.org/downloads/">site</a> for a guide on how to download and install Python for your specific operating system.</p>
</li>
<li><p>Pipenv or any Python virtual environment creation tool. You can find the Pipenv installation guide <a target="_blank" href="https://pypi.org/project/pipenv/">here</a><em>.</em></p>
</li>
<li><p>A basic knowledge of Git.</p>
</li>
<li><p>An API client, like Postman or Thunderclient.</p>
</li>
</ul>
<h2 id="heading-project-goal">Project Goal</h2>
<p>By the end of this project, you should be able to deploy APIs in Lambda securely, leveraging AWS cloud-native security services.</p>
<h2 id="heading-project-overall-architecture">Project Overall Architecture</h2>
<p>Below is the architecture of the project workflow:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758829544078/b76347ee-bbd3-41f4-88c8-2b3a89ad9087.png" alt="Project Architectural Diagram" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>As shown in the architectural diagram, when a user sends a request (a JSON object consisting of the user’s name) to an API hosted in Lambda, the user first gets authenticated by an authentication service called Amazon Cognito.</p>
<p>The request passes through a Web Application Firewall, then an API Gateway. API Gateway will perform a check to see if the user is authorized to access the API using the token that the user sends with the request after authentication. API Gateway then allows the traffic to pass through to the API if the user is authorized.</p>
<p>The user’s request will first get to an External Lambda function, which will then save the user’s name as a message to a Simple Notification Service (SNS) topic. This will then invoke an Internal Lambda to run and log the output in Amazon CloudWatch logs. The SNS topic will be accessed by External Lambda using the SNS’s unique identifier stored in Amazon Secrets Manager.</p>
<h3 id="heading-aws-set-up">AWS Set Up</h3>
<p>You’ll need to set up an AWS environment to get started. This requires creating an account if you don’t already have one.</p>
<p>Following account creation, a root user is automatically created, with all privileges attached to the user. Security best practice is to create another user with administrator privileges and use this user for subsequent tasks.</p>
<p>Then, create an access key for this user, which usually consists of two parts (Access Key ID and Secret Access Key) by navigating to the following:</p>
<p>IAM —→ Users —→ Create Access key</p>
<p>Follow the prompts and choose the <code>Command Line Interface</code> option. Check the <code>Confirmation</code> box, and go on to create the key. Download the CSV file provided, or manually copy the <code>Access Key ID</code> and <code>Secret Access Key</code>. Save them securely.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758890397608/a88ec1c6-511c-4a66-aa7a-d0dd3f41f665.png" alt="IAM Dashboard" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758890497481/faab2cb7-b7ba-4e5c-b67e-a00d8fb27a10.png" alt="IAM User Page" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758890928429/1a4b3163-6340-47d2-be3e-0e61c275ba8f.png" alt="Create Access Key Page" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758890991928/5bb150b6-b014-4398-b839-ee5d6e49c425.png" alt="Access Key Use Option Page" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758891021874/a2e4eb61-eaca-4732-9377-b499fa7eab5d.png" alt="Set Access Key Tag Page" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758891049362/372f3a63-8e64-478d-9b06-61f7aa88f73a.png" alt="Download Access Key" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Open up your terminal and run the following commands using the AWS CLI:</p>
<pre><code class="lang-bash">aws configure
</code></pre>
<p>The above command will give some prompts to provide the components of the <code>Access Key</code> created earlier and your default region (the AWS region hosting the service you intend to interact with).</p>
<h3 id="heading-clone-project">Clone Project</h3>
<p>In the next step, you’ll clone the GitHub repository containing the assets and resources used in the project implementation.</p>
<p>Visit the project <a target="_blank" href="https://github.com/Agnes4Him/secure-lambda">URL</a> and clone the repository locally.</p>
<pre><code class="lang-bash">git <span class="hljs-built_in">clone</span> &lt;repository_clone_url&gt;
</code></pre>
<h3 id="heading-set-up-simple-notification-service">Set Up Simple Notification Service</h3>
<p>Amazon Simple Notification Service (SNS) connects system components, enabling asynchronous communication and messaging among them.</p>
<p>Find <code>SNS</code> on the console, click on it, and create a topic that your APIs will send messages to. After successfully creating a topic, navigate to the topic, and in the topic details, you’ll find the topic’s <code>ARN</code>. An ARN is an Amazon Resource Name, and it’s a unique string attached to a resource you’ve created on AWS to help identify the resource. Copy the <code>ARN</code> of the topic.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758983690093/a2820581-46fb-41d1-aed9-9471a0c2db02.png" alt="SNS Dashboard" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758982964553/3eb717c7-8ce3-497b-96fb-c16483cff43e.png" alt="Create SNS Topic" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758983004729/854335ec-3d53-42e4-8bef-0e8a7d3fb2e6.png" alt="Topic Details" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758983094356/320ebaf9-9f2d-4241-b747-fd3fd0f0b62b.png" alt="SNS Topic Access Policy" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758983451385/7886c2c4-a52f-4538-8e9d-0d33738f7632.png" alt="Topic Created" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-set-up-secrets-manager">Set Up Secrets Manager</h3>
<p>Amazon Secrets Manager is used to store, manage, and retrieve sensitive information such as keys, credentials, tokens, and so on. You’ll store the <code>Topic ARN</code> created earlier. With this approach, you’ll demonstrate how your API can securely access the data and information it needs for its performance.</p>
<p>Go to <code>Secrets Manager</code> on the AWS console and create a secret. Provide the secret’s details, and add a new secret named <code>TOPIC_ARN</code> as the key and the actual SNS Topic ARN as the value.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758984342157/38c7855c-2221-4406-9078-496cbb480e47.png" alt="Secrets Manager Console" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758984379959/3c4ebdf7-26c8-4b74-a175-0ea2eefd258d.png" alt="Create Secret" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758984459477/cfdd8fce-4a33-45c3-8f12-d6a4a04bf799.png" alt="Choose Other Types of Secret" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758984512368/7a76618e-18f6-4b3d-bc03-5941c89909ef.png" alt="Secret Details" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1758984575543/91ca9360-6320-442a-a121-37f9f35b175b.png" alt="Final Secret Store" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Next, you’ll create some Lambda functions to serve your APIs and consume the output of the APIs. There’re three Lambda functions to set up. Two of the functions will host APIs, each of which can only be accessed by specific users. These will be referred to as <code>ExternalLambda</code>. The third Lambda will consume the output of the External Lambda functions through SNS.</p>
<h3 id="heading-set-up-internal-lambda">Set Up Internal Lambda</h3>
<p>AWS Lambda is a serverless service on AWS that users can leverage to run application functions or code when needed. You’re billed for your Lambda function based on the number of invocations of the function, the duration each invocation lasted, and the amount of memory allocated to the function. Lambda can be provisioned to use any runtime, such as Python or NodeJS. In this demonstration, you’ll focus on the NodeJS runtime.</p>
<p>Now that you know what Lambda is and does, you can create one. Let’s call the first Lambda function InternalLambda. On the AWS console, search for <code>Lambda</code>, and on the <code>Lambda</code> dashboard, click <code>Create a function</code> and provide the details. We’ll be using <code>Node.js</code> – JavaScript at the backend as the runtime of choice.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759140048151/60d5c813-a190-456e-9bad-50b429bdc6f7.png" alt="AWS Lambda" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759140165012/d519f8e9-cb98-4f75-b94e-d4d343e3003c.png" alt="Lambda Details" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>For the <code>Permissions</code> details, let Lambda create a default <code>IAM Role</code>. This default role is named according to your function, and the permissions attached to the role allow your Lambda function to send logs to CloudWatch, another AWS service used for monitoring and observability.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759140368576/3f922a46-7b3c-4034-b20b-c2b9ab5dde94.png" alt="Lambda Permissions" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759146593382/39d50020-d0bf-4cfe-950f-eec8a2ff8989.png" alt="39d50020-d0bf-4cfe-950f-eec8a2ff8989" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>As you can see in the last image above, the Lambda function you’ve created needs a <code>trigger</code> and sometimes, a <code>destination</code>. For your <code>InternalLambda</code>, the trigger is the SNS topic we configured earlier. This Lambda will read the messages that’ve been published to it, and then you can access the message from your client or even CloudWatch logs.</p>
<p>To achieve this, click the <code>Add trigger</code> button and provide the details.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759140925997/2534535f-5ad3-4e13-99f1-d8bf48c9cec1.png" alt="Add SNS to Lambda" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759146639798/1f3d75c9-ef5d-4538-9f3a-aaf2e8c0ddbb.png" alt="SNS ARN" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759146670136/367f7ca9-0b41-41ed-8749-ff70e1770ebb.png" alt="InternalLambda Overview" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Next, you’ll provide the <code>code</code> you want to invoke through Lambda. Find the code in the GitHub repository that you cloned earlier. Paste the code in the Lambda function code space and click on <code>Deploy</code> to deploy the function.</p>
<p><code>secure-lambda/InternalLambda/index.js</code></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = <span class="hljs-keyword">async</span> (event) =&gt; {
    <span class="hljs-keyword">try</span> {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Request successfully received from SNS'</span>);                            

        <span class="hljs-keyword">let</span> name = event[<span class="hljs-string">'Records'</span>][<span class="hljs-number">0</span>][<span class="hljs-string">'Sns'</span>][<span class="hljs-string">'Message'</span>];
        <span class="hljs-keyword">let</span> response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">`Hello <span class="hljs-subst">${name}</span>. Greetings from InternalLambda!`</span>),
        };       
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Response: '</span>, response);                                                
        <span class="hljs-keyword">return</span> response;
    } <span class="hljs-keyword">catch</span> (err) {                            
        <span class="hljs-keyword">let</span> response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">'An error occurred while processing your request.'</span>),
        };

        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error processing event'</span>, err);
        <span class="hljs-keyword">return</span> response;
    }   
};
</code></pre>
<p>The function defined in the index.js file above is simply taking the <code>event</code> object sent to it from SNS and extracting the <code>Message</code> attribute within it. We’re using <code>console.log</code> here to view outputs from the function and ensure it behaves as expected. Just don’t use this in a production-ready application.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759142277747/f4437ff2-4495-485d-b891-d9dda3fc939c.png" alt="InternalLambda Code" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-set-up-external-lambda">Set Up External Lambda</h3>
<p>You’ll be creating two external Lambda functions: 1 and 2. These two functions will receive user requests, process them, and publish messages to your SNS topic.</p>
<p>On the Lambda console, create another function and name it <code>ExternalLambda1</code>. Allow Lambda to create a default IAM Role, as previously.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759144966306/ee8a2ed1-5a2e-48df-8556-5dedd7ecdde1.png" alt="Create ExternalLambda1" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759146732803/82a46fd1-e3e5-4d72-a9fe-b41496ba076b.png" alt="ExternalLambda1 Overview" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Paste the code snippet below in the <code>ExternalLambda1</code> code space:</p>
<p><code>secure-lambda/ExternalLambda1/insex.js</code></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> {
  GetSecretValueCommand,
  SecretsManagerClient,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-secrets-manager"</span>;

<span class="hljs-keyword">import</span> { SNSClient, 
    PublishCommand 
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-sns"</span>;

<span class="hljs-keyword">const</span> secretsManagerClient = <span class="hljs-keyword">new</span> SecretsManagerClient();

<span class="hljs-keyword">const</span> snsClient = <span class="hljs-keyword">new</span> SNSClient({});

<span class="hljs-comment">// Fetch topicArn from AWS Secrets Manager</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSecretValue</span>(<span class="hljs-params">secretName</span>) </span>{
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> secretsManagerClient.send(
                            <span class="hljs-keyword">new</span> GetSecretValueCommand({
                            <span class="hljs-attr">SecretId</span>: secretName,
                            }),
                        );
        <span class="hljs-keyword">if</span> (data.SecretString) {
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(data.SecretString);
        }   <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">let</span> buff = Buffer.from(data.SecretBinary, <span class="hljs-string">'base64'</span>);
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(buff.toString(<span class="hljs-string">"utf-8"</span>));
        }
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error retrieving secret'</span>, err);                             <span class="hljs-comment">// added for debugging</span>
        <span class="hljs-keyword">throw</span> err;
    }
}                                        

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = <span class="hljs-keyword">async</span> (event) =&gt; {

    <span class="hljs-keyword">let</span> name = event[<span class="hljs-string">'name'</span>];
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Request successfully received from <span class="hljs-subst">${name}</span>`</span>);    

    <span class="hljs-comment">// Retrieve SNS Topic ARN from Secrets Manager</span>
    <span class="hljs-keyword">let</span> topicArn;
    <span class="hljs-keyword">let</span> response;
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> secret = <span class="hljs-keyword">await</span> getSecretValue(<span class="hljs-string">'LambdaSNSTopicARN'</span>);
        topicArn = secret.TOPIC_ARN;
    } <span class="hljs-keyword">catch</span> (err) {
        response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">'An error occured, try again later.'</span>),
        };
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Failed to load SNS Topic ARN from Secrets Manager'</span>, err);
        <span class="hljs-keyword">return</span> response;        
    }

    <span class="hljs-comment">// Publish to SNS topic</span>
   <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> snsResponse = <span class="hljs-keyword">await</span> snsClient.send(
        <span class="hljs-keyword">new</span> PublishCommand({
            <span class="hljs-attr">Message</span>: name,
            <span class="hljs-attr">TopicArn</span>: topicArn,
        })
        );
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Message published successfully:"</span>, snsResponse.MessageId);
        response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">`Hello <span class="hljs-subst">${name}</span>. Greetings from ExternalLambda1! Message forwarded to InternalLambda.`</span>),
        };
        <span class="hljs-keyword">return</span> response;
  } <span class="hljs-keyword">catch</span> (err) {
        response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">`Sorry <span class="hljs-subst">${name}</span>.An error occurred while processing your request.`</span>),
        };
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Failed to publish message:"</span>, err);
        <span class="hljs-keyword">return</span> response;
  }  
};
</code></pre>
<p>The code above leverages the AWS SDK to fetch the ARN of the SNS topic created earlier from Secrets Manager. It then publishes a message to the topic.</p>
<p>The SDK already comes installed in the Lambda function. Outside of Lambda, the SDK has to be explicitly installed. The function receives its <code>event</code> from the client via API Gateway, which we’ll configure later.</p>
<p>The SNS topic you created earlier will be the destination for this function. For Lambda to publish a topic to SNS, it needs the necessary permission attached to its IAM Role. AWS can automatically take care of that during your configuration, as shown below.</p>
<p>For the trigger, you’ll use another service known as <code>API Gateway</code>. More on that later.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759146816330/3c542eff-984e-4d02-85b3-c1da142f94d7.png" alt="ExternalLambda1 Add Destination" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759146778567/9b3650f1-90fd-47ce-99c3-627654f2d41f.png" alt="ExternalLambda1 Destination Permissions" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Follow the same steps to provision another Lambda known as <code>ExternalLambda2</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759145915181/631aa639-493a-4f12-af1e-45f425ca2c16.png" alt="ExternalLambda2" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>The outcome of the External Lambda setup is as shown below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759146919917/93aaf281-387f-44c3-bf6d-995b076150e9.png" alt="ExternalLambda2 Overview" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Paste the code below into <code>ExternalLambda2</code>. It performs the same function as <code>ExternalLambda1</code>, but their output differ. Each of the two Lambda functions will be receiving traffic to a specific user that’s authorized to access the function.</p>
<p><code>secure-lambda/ExternalLambda2/index.js</code></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> {
  GetSecretValueCommand,
  SecretsManagerClient,
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-secrets-manager"</span>;

<span class="hljs-keyword">import</span> { SNSClient, 
    PublishCommand 
} <span class="hljs-keyword">from</span> <span class="hljs-string">"@aws-sdk/client-sns"</span>;

<span class="hljs-keyword">const</span> secretsManagerClient = <span class="hljs-keyword">new</span> SecretsManagerClient();

<span class="hljs-keyword">const</span> snsClient = <span class="hljs-keyword">new</span> SNSClient({});

<span class="hljs-comment">// Fetch topicArn from AWS Secrets Manager</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSecretValue</span>(<span class="hljs-params">secretName</span>) </span>{
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> data = <span class="hljs-keyword">await</span> secretsManagerClient.send(
                            <span class="hljs-keyword">new</span> GetSecretValueCommand({
                            <span class="hljs-attr">SecretId</span>: secretName,
                            }),
                        );
        <span class="hljs-keyword">if</span> (data.SecretString) {
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(data.SecretString);
        }   <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">let</span> buff = Buffer.from(data.SecretBinary, <span class="hljs-string">'base64'</span>);
            <span class="hljs-keyword">return</span> <span class="hljs-built_in">JSON</span>.parse(buff.toString(<span class="hljs-string">"utf-8"</span>));
        }
    } <span class="hljs-keyword">catch</span> (err) {
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error retrieving secret'</span>, err);  
        <span class="hljs-keyword">throw</span> err;
    }
}                                        

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> handler = <span class="hljs-keyword">async</span> (event) =&gt; {

    <span class="hljs-keyword">let</span> name = event[<span class="hljs-string">'name'</span>];
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Request successfully received from <span class="hljs-subst">${name}</span>`</span>);    

    <span class="hljs-comment">// Retrieve SNS Topic ARN from Secrets Manager</span>
    <span class="hljs-keyword">let</span> topicArn;
    <span class="hljs-keyword">let</span> response;
    <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> secret = <span class="hljs-keyword">await</span> getSecretValue(<span class="hljs-string">'LambdaSNSTopicARN'</span>);
        topicArn = secret.TOPIC_ARN;
    } <span class="hljs-keyword">catch</span> (err) {
        response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">'An error occured, try again later.'</span>),
        };
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Failed to load SNS Topic ARN from Secrets Manager'</span>, err);
        <span class="hljs-keyword">return</span> response;        
    }

    <span class="hljs-comment">// Publish to SNS topic</span>
   <span class="hljs-keyword">try</span> {
        <span class="hljs-keyword">const</span> snsResponse = <span class="hljs-keyword">await</span> snsClient.send(
        <span class="hljs-keyword">new</span> PublishCommand({
            <span class="hljs-attr">Message</span>: name,
            <span class="hljs-attr">TopicArn</span>: topicArn,
        })
        );
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Message published successfully:"</span>, snsResponse.MessageId);
        response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">200</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">`Hello <span class="hljs-subst">${name}</span>. Greetings from ExternalLambda2! Message forwarded to InternalLambda.`</span>),
        };
        <span class="hljs-keyword">return</span> response;
  } <span class="hljs-keyword">catch</span> (err) {
        response = {
            <span class="hljs-attr">statusCode</span>: <span class="hljs-number">500</span>,
            <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify(<span class="hljs-string">`Sorry <span class="hljs-subst">${name}</span>.An error occurred while processing your request.`</span>),
        };
        <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Failed to publish message:"</span>, err);
        <span class="hljs-keyword">return</span> response;
  }              
};
</code></pre>
<p>Before moving on, you need to modify the External Lambda’s IAM Roles. Currently, IAM Roles only have permissions to write to CloudWatch and SNS (automatically added). External Lambda also needs permission to fetch the ARN of the SNS topic that was created earlier.</p>
<p>The point here is to show how to leverage a secrets manager, such as AWS Secrets Manager, to store sensitive information or data, and still access these securely. This approach is more secure than storing the ARN as an environment variable within Lambda.</p>
<p>Navigate to IAM, and click on <code>Policies</code> tab on the left. This brings you to a list of policies. Next, click on <code>Create policy</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759320098124/71bde9ad-c6d9-4c0d-8472-d56107708be2.png" alt="IAM Policies" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Search for <code>secrets manager</code> in the Policy editor.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759320163875/a040af9a-1e92-4aea-8c2e-5c029f60f54e.png" alt="Policy Editor" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759320923537/c3e8bb54-e78d-498c-9fec-7c7a5225b116.png" alt="Policy Editor2" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Select the permissions Lambda needs to access Secrets Manager. In this case, that would be <code>Read —&gt; GetSecretValue</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759321060458/963c1876-dbcc-4d7d-a6fe-9f65281c1a26.png" alt="Policy Editor - Specify Permissions" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Select <code>Specific</code> for Resources, and click on <code>Add ARNs</code>. On the next tab, add the details of the Secrets Manager Secret created earlier.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759321219657/f50fc354-a238-499c-9009-958bbc624299.png" alt="Policy Editor - Select Access" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>The Secret’s ARN will be populated here.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759321662642/356fb6bd-adc7-4663-a337-3cfaedb74b2d.png" alt="Policy Editor - Add Secrets Manager ARN" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Next, give the policy a name and create it.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759321510186/fa5da448-293f-4d95-a3b5-651292a91a7f.png" alt="Policy Editor - Create Policy" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759321721882/27807dda-8ea3-4489-bcab-d03efc201655.png" alt="Newly Added Policy" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Next, navigate to <code>Roles</code>, and search for the IAM Roles assigned to the External Lambda functions. These are named according to the Lambda.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759321748368/dfc73acb-622c-44f9-9cf8-be51b31e3fe9.png" alt="IAM Roles" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759321856410/f1d4a13c-a568-4c9c-b14f-bb3d24b870f8.png" alt="Lambda IAM Roles" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Click <code>Add permissions</code> to add a new permission to the IAM Role selected.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759322020293/689715ef-7e8c-45cb-9473-010f5aa105fa.png" alt="ExternalLambda1 Role" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759322453532/83996cca-7a05-48fb-8e31-d3fc679df7bc.png" alt="ExternalLambda1 Role - Policy Added" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759322498243/29a8fff5-af9a-4d4c-b3ff-3790b82b6339.png" alt="ExternalLambda2 Role" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759322570298/ab28750d-eb99-40f3-bf48-936b63bba1f0.png" alt="ExternalLambda2 Role - Policy Added" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-configure-web-application-firewall">Configure Web Application Firewall</h3>
<p>A firewall is a system placed in front of an application, workload, APIs, and so on to inspect traffic, filter it, and either allow or block the traffic based on some preconfigured rules.</p>
<p>For this project, you’ll use the AWS Web Application Firewall (WAF) service to inspect user requests before routing the traffic to your APIs running in Lambda.</p>
<p>Head over to the AWS console and search for WAF.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759310730367/bbcbdf00-2759-4dd7-9b7b-63ee9c252542.png" alt="AWS Web Application Firewall" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Click on the <code>IP sets</code> tab on the left. This will enable you to create a list of IP addresses that you want to allow (as in this case) or deny.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759311298551/4537577d-a574-417e-8352-3f72b3732926.png" alt="IP Sets Page" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759311354043/edd29c9c-63e7-4bf6-a503-23ef0af5ac20.png" alt="IP Set Configuration" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>The IP addresses should include a CIDR block. For instance, if adding a single IP address, it should be <code>X.X.X.X/32</code>. The same applies to IP address ranges such as <code>X.X.X.X/24</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759311560565/0ad16e51-b70b-4a80-98f4-821659fa61b8.png" alt="IP Set Overview" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Next, click on the <code>Web ACLs</code> tab, then <code>Create web ACL</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759311623780/9742ab87-3303-4046-84df-f9f770ed7c41.png" alt="Web ACL Page" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Choose <code>Regional resources</code> as the Resource type, and enter your region. It’s best to keep all resources you’re creating in this project within the same region. Give your Web ACL a name, then click next.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759311736091/b4201885-2dc0-4ed8-aa38-5e25824c363b.png" alt="Web ACL Description" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Add rules to the Web ACL.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759311892739/5efad662-6c20-4678-b490-54fa33bc3a7b.png" alt="WAF Rule" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759311985197/9e7157c8-bfb4-47a9-a850-67ce8bb302b2.png" alt="Add Rule" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Choose a rule type. In this case, you’ll use <code>IP set</code>, and give the rule a name. Choose the IP set created earlier.</p>
<p>Select <code>Source IP address</code>, and <code>Count</code> as the Action. For this project, you’ll focus on counting the requests sent to your APIs. But as shown in the image below, you can perform other actions, such as allow, block, and so on.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759312925911/ea491527-970c-4b5f-b658-4345ce3d08e4.png" alt="WAF Rule Configuration" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Your final rule configuration will appear this way.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759313133810/8e1be6d3-f6bf-42d9-881d-87216862b3bd.png" alt="WAF Rule Overview" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Scroll down, then click on <code>Create web ACL</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759313210947/d625c4f3-a5a3-47c9-961f-ca67f652c992.png" alt="Create Rule" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759313261493/3bd16ec5-3376-4607-86cc-fd1716ad68aa.png" alt="Web ACL Dashboard" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-configure-cognito-user-pools">Configure Cognito User Pools</h3>
<p>Amazon Cognito is an identity management service used for creating and managing users. You can leverage it to authenticate and authorize users to applications, APIs, or other workloads.</p>
<p>You’ll create <code>User Pools</code> within Cognito and add a user to each pool. You’ll configure how these users can be authenticated and authorized to access the External Lambda functions already created.</p>
<p>Search for <code>Cognito</code> on AWS.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759315681568/c2e7df4e-0e51-4c03-bf59-41ca895df74d.png" alt="Amazon Cognito" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Click on <code>Get started for free</code>, then <code>Create user pool</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759315735324/1c09e934-186f-49db-811f-dd84d7400285.png" alt="Create User Pool" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Select Single-page application (SPA), give the User pool the name <code>MyUserPool1</code>, and select <code>Email</code> as an option for sign-in. This means the main attribute users will provide at signup and sign-in will be their email address. Leave everything else as the default. We’ll keep things as simple as possible.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759315828576/73cb66a3-cbde-4443-8dfd-34338091aabc.png" alt="Use Pool Configuration" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759315901551/9fdc173d-f92b-4080-98d4-513b404a9aeb.png" alt="User Pool Configuration2" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759315994247/3fb17ade-90aa-4d47-b2f9-258f6b547a1f.png" alt="User Pool Configuration3" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>After creating the User pool, you’ll find the page shown below. You can view the login and signup page for the pool you’ve just created by clicking on the <code>View login page</code> button.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759316208497/4db5f370-deb2-449e-8017-505dc1e13079.png" alt="Cognito App Client Login URL" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>You can add <code>App clients</code> to your User Pool. By default, a client named <code>MyUserPool1</code> will be added to the pool. Navigate to your User pool, and click on <code>App clients</code> to see details of this client. Note the <code>Client ID</code>. You’ll also make some edits to the App client by clicking on the <code>Edit</code> button.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759316443170/58081c40-cdcb-4af9-a60b-79156f6d2d68.png" alt="User Pool App Client Overview" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>You’ll edit the <code>Authentication flows</code> field by ticking the <code>Sign in with username and password…</code> and <code>Sign in with server-side administrative credentials…</code> boxes. These changes will enable you to authenticate the user who will be added to this client programmatically, rather than through a UI. With this approach, we can fetch the token assigned to the user by Cognito and use this token to authorize access to Lambda.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759317429341/f8db3816-c603-49fe-b661-696bfff98639.png" alt="Edit App Client" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Now, add a user to this pool. The user needs a valid email address. You’ll need the login page URL to create the user.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759318555729/d9a8ac0c-72d4-4fca-8a94-ff71a5a20caf.png" alt="Cognito Create New User" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>You need access to the email used to create the user. Fetch the code sent to the email address and submit to confirm the account.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759318672240/42d4418d-a4e1-4af9-b8b5-deaf5fb63118.png" alt="Cognito Confirm Email" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759318710760/4505260f-13c9-4b3b-af99-1cb9e7436147.png" alt="Cognito Successful Sign up" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759318734000/1b3789d9-917a-4341-84ef-b8e498628557.png" alt="User Pool Users" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Follow the same steps and create another User pool named <code>MyUserPool2</code>. Add a user with a different email to this pool.</p>
<h3 id="heading-configure-api-gateway">Configure API Gateway</h3>
<p>API Gateway is a service used to manage access and route traffic to API backend services such as APIs. It serves as a reverse proxy and provides an extra layer of security for backend services.</p>
<p>You’ll configure API Gateway to direct traffic to your Lambda functions.</p>
<p>Navigate to <code>API Gateway</code> and click on <code>Create an API</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759336384538/0e2e4120-ade3-43c3-8a3e-9ec4d0e2b343.png" alt="API Gateway" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Select the <code>REST API</code> option —→ <code>Build</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759336533762/56d96fcb-ff27-4f96-b739-fc9658dca50e.png" alt="Select API Type" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Select <code>New API</code>, provide a name, and choose <code>Regional</code> as the API endpoint type. IP address type can be IPv4 or Dualstack. We’ll select IPv4 here. Then create.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759336585590/ff2b11c2-cb32-4657-913e-6dfc9922531f.png" alt="API Gateway Configuration" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>An important part of API Gateway configuration for this project is the Authorizer. API Gateway uses Authorizer to allow traffic from clients to backend services.</p>
<p>You’ll create two Authorizers. Each will be connected to one of the User pools you configured earlier. On the left-hand side of the API Gateway you configured, click on <code>Authorizers</code> —→ <code>Create authorizer</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759336744757/bb00a260-3897-4867-96d3-5370e40eae59.png" alt="API Gateway Authorizer" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Provide the name <code>AGAuthorizer1</code>, and select <code>Cognito</code> as the Authorizer type. Add the User pool for MyUserPool1 created earlier. For the Token source, use <code>Authorization</code>. When you send a request from your API client, a token will be added to the request header for authorization. The token’s key will be named <code>Authorization</code>, while the value will be the token itself.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759337066816/1ef4b11c-3508-49a5-ae7b-561b4c7f4259.png" alt="Authorizer1 Configuration" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Create another Authorization for MyUserPool2 the same way.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759337472200/aacad54f-8927-4c3e-9a60-f207bbf45577.png" alt="Authorizer2 Configuration" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Both Authorizers will appear this way.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759337540381/56362d9c-7f84-4021-b077-59992abd979b.png" alt="Authorizers Overview" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Next, you’ll create resources and endpoints within the API Gateway that you’ve defined.</p>
<p>A <code>resource</code> in API Gateway is used to group certain endpoints within a specific path. You’ll define two resources within the API Gateway you’ve created. This will create two different paths, &lt;BASE_URL&gt;/&lt;RESOURCE1&gt; and &lt;BASE_URL/RESOURCE2&gt;.</p>
<p>On the API Gateway dashboard, navigate to your Gateway, click on <code>Create resource</code>, define your root path (‘/’ in your case), and provide the resource name (<code>lambda1</code>).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759420480646/c6e7c1c9-9eee-4dbf-af5b-8c335e14927c.png" alt="API Gateway Lambda1 Resource" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Create another resource named <code>lambda2</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759420822042/74d1790e-c752-490c-883f-b42bd00d91eb.png" alt="API Gateway Resources Overview" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Now, click on <code>/lambda1</code>, then <code>Create method</code> to define an endpoint within this resource. You’ll use the <code>POST</code> method to send requests to the backend service via this endpoint.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759420769955/bd861a6a-884d-4873-aaeb-dc958a0915b1.png" alt="API Gateway Method Configuration" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>For the backend service or Integration type, select Lambda function, and provide the ARN of ExternalLambda1.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759421208532/8d84c272-58ad-4ca4-ae56-682151495b76.png" alt="API Gateway Method Configuration2" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>For Authorization, select <code>AWS IAM —→ Cognito user pool authorizers —→ AGAuthorizer1</code>. Leave other configurations, then create the endpoint.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759421234684/1e76c46e-ba07-4178-94b3-e579ed752278.png" alt="API Gateway Method Configuration3" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Repeat the same step to create a <code>POST</code> method for <code>/lambda2</code> resource. The <code>method</code> should be attached to <code>ExternalLambda2</code>, and <code>AGAuthorizer2</code>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759421691530/563c85e5-52ff-43fa-8216-41fc269989e0.png" alt="API Gateway Deployment" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>The API Gateway you’ve created needs to be deployed to become accessible. Deployment is usually done to a Stage.</p>
<p>Click on <code>Deploy API</code>, select New stage and name the stage development. Then, deploy.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759421954217/f0ca31c8-78d9-4b1b-a47c-424f6ef32093.png" alt="API Gateway Stage" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>After deployment to a stage, an invoke URL will be provided. This will serve as the base URL for the endpoints you’ve defined.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759422031311/c7cac4e7-52e9-43dd-a565-46aa062aa364.png" alt="API Gateway Stage Overview" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>The stage you’ve created needs some modifications for enhanced security. Firstly, you need to attach the <code>WAF</code> that you created earlier. Secondly, the default rate limit for the API deployed to this stage is 10000. Rate limit restricts excessive resource consumption and protects your API from abuse. For this project, you can reduce the limit to 50.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759422132101/b0f67d30-30ec-4d1f-9eee-735dc3b26500.png" alt="Edit API Gateway Stage" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759423441047/95dc8f74-c71d-449a-b15a-46caa53c7595.png" alt="API Gateway Stage - Add Rate Limit and WAF" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>To test the API Gateway set up, click on the endpoint you want to test, then the <code>Test</code> button. This initial test doesn’t need any authorization, since the test is done directly within the Gateway.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759435394293/8933349c-3e2a-4795-adb4-bb9eeb990e81.png" alt="API Gateway Endpoint Testing" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Add JSON data as the Request body. The key will be <code>name</code>, and the value will be any string.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759435424778/a613816e-8b89-4978-9b3e-6734e48119eb.png" alt="API Gateway Testing2" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>The response sent back from ExternalLambda1 shows a status code of 200, and a response body containing exactly the message expected from the Lambda function.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759435744694/516bab58-99de-4b41-8926-e9928b8c42e4.png" alt="API Gateway Test Response" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>If you head over to CloudWatch Log groups, you should also find the Log groups that were automatically created for the Lambda functions. Click on the Log group for ExternalLambda1 and navigate to the latest Log stream. You should find the logs for the request you’ve just made from API Gateway.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759435884121/05741f0c-82dd-43f1-855c-157df5c112fc.png" alt="CloudWatch Logs for Testing" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759436073425/d4a0a2c7-8d86-44ce-adb7-4c67c3cdf40b.png" alt="CloudWatch Logs for Testing2" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759436150374/5fae1a11-c80d-41df-8ea9-39303287144b.png" alt="CloudWatch Logs - Output from InternalLambda" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-test-setup-end-to-end">Test Setup End-to-End</h3>
<p>To test our setup properly, and from the internet, send the same request from your API client with no additional information in the request header. This should return a <code>401</code> error – Unauthorized. This is expected.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759436452961/342e7659-5a58-45a2-af26-a657b622a83a.png" alt="Request without Token" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>API Gateway expects an authorization token from each request it receives before routing traffic to the appropriate backend service. It validates this token through Cognito.</p>
<p>You’ll mimic a user login for each user added to Coginito User pools to get a token for the user. This token will then be sent alongside any request. To achieve this, you’ll use the two Python scripts I’ve provided below:</p>
<p><code>secure-lambda/auth-scripts/user1.py</code></p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> boto3

client = boto3.client(<span class="hljs-string">"cognito-idp"</span>)

response = client.initiate_auth(
    AuthFlow=<span class="hljs-string">"USER_PASSWORD_AUTH"</span>,  <span class="hljs-comment"># or ADMIN_USER_PASSWORD_AUTH if using admin creds</span>
    AuthParameters={
        <span class="hljs-string">"USERNAME"</span>: <span class="hljs-string">""</span>,             <span class="hljs-comment"># user1 email</span>
        <span class="hljs-string">"PASSWORD"</span>: <span class="hljs-string">""</span>              <span class="hljs-comment"># user1 password</span>
    },
    ClientId=<span class="hljs-string">""</span>                     <span class="hljs-comment"># Cognito App Client ID</span>
)

id_token = response[<span class="hljs-string">"AuthenticationResult"</span>][<span class="hljs-string">"IdToken"</span>]
access_token = response[<span class="hljs-string">"AuthenticationResult"</span>][<span class="hljs-string">"AccessToken"</span>]
refresh_token = response[<span class="hljs-string">"AuthenticationResult"</span>][<span class="hljs-string">"RefreshToken"</span>]

print(<span class="hljs-string">"ID Token:"</span>, id_token)
</code></pre>
<p>Using the Python boto3 library, you’ll initiate an authentication request to Cognito. Provide the email address and password of the user in MyUserPool1. Also, add the Client ID of the App client.</p>
<p>To run the script, create an isolated environment using Pipenv, uv, or a similar library. Install the dependency used in the project as defined in the Pipfile, and run the script with the Pipenv shell.</p>
<pre><code class="lang-bash">pipenv install
pipenv shell
Python secure-lambda/auth-scripts/user1.py
</code></pre>
<p>The Python command will return with a token assigned to the user. Next, you use this token to authorize a user to access ExternalLambda1.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759437804885/f03ab150-74f9-4ecf-9c7c-0ee07e8662a2.png" alt="Add Token to Request Header" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Ensure that the URL for the POST request is in the format: &lt;BASE_URL/lambda1&gt;. You should receive a response from API Gateway indicating success.</p>
<p>Now try accessing ExternalLambda2 using User1 token. You should get an <code>Unauthorized</code> message. Note that user1 will always receive an unauthorized message when it tries accessing ExternalLambda1 without an Authorization token in the header, a wrong token, or when it tries accessing ExternalLambda2, which it is not authorized to access.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438020972/39e2650a-6a99-466a-ad40-6bcf72c491c4.png" alt="User1 Access ExternalLambda2" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Repeat the process with User2 using the token generated for the user in MyUserPool2. First, test access to ExternalLambda2 without a token in the request header.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438058029/37dbffcb-7f58-4ce0-abfa-abf0e6d1d3a4.png" alt="User2 Request without Token" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Then test access with the token.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438118097/a408bfe0-6e94-46a1-b98e-c959f31673f8.png" alt="User2 Request with Token" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Next, try accessing ExternalLambda1 using User2.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438144636/06ba905d-ff7b-48d6-8e5b-e35b959221ba.png" alt="User2 Access ExternalLambda1" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>You can also view the outcome of some of the requests made by your client on CloudWatch Logs.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438188476/2e5aaa35-b52c-4fd3-88f4-00bc704cd809.png" alt="CloudWatch Logs Output" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438223720/630bd443-41d2-40b7-b28f-ff937fbf13f9.png" alt="CloudWatch Logs Output2" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Also, since WAF has been configured previously to count requests (although, in a real scenario, you want to achieve much more with WAF, such as allow or block certain traffic), you can view activities captured by WAF by navigating to the service on AWS, then searching for the WAF you configured, and navigating to Traffic overview.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438469300/df718ef7-7eaa-49ad-8780-c43878d2d388.png" alt="WAF - Traffic details" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438498828/b1619abf-db45-4946-85e2-53e5a769cdb8.png" alt="WAF - Traffic Details2" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>You can find other details, such as the client device types and where requests originated.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438552359/477c4f39-04e4-4427-a2d2-74a6066622dd.png" alt="WAF - Traffic Details3" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1759438590513/31acecd0-7f45-4fb9-b352-5dc5fcf49e75.png" alt="WAF - Traffic Details4" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<h3 id="heading-clean-up">Clean Up</h3>
<p>It’s important to clean up the resources created so far after the hands-on exercise. Due to the dependencies among the resources, trying to delete a resource that another resource depends on may lead to an error. So, you should delete them in this order:</p>
<ul>
<li><p>Secrets Manager</p>
</li>
<li><p>Cognito – Users, App Client, then User Pool</p>
</li>
<li><p>API Gateway – Endpoints/ Methods, Resources, API, Stage</p>
</li>
<li><p>Web Application Firewall – IP Set, Web ACL</p>
</li>
<li><p>All Lambda Functions</p>
</li>
<li><p>Lambda IAM Roles and the policies attached to them</p>
</li>
<li><p>CloudWatch Log Group for all the Lambda functions</p>
</li>
<li><p>SNS Topic</p>
</li>
</ul>
<p>Also, you can deactivate or delete the credentials created for your IAM Admin user if not in use.</p>
<h2 id="heading-improvements">Improvements</h2>
<p>Consider the following areas to improve, apply best practices to, and enhance the security posture of your systems further.</p>
<ol>
<li><p>Use of API keys</p>
</li>
<li><p>Third-party API consumption</p>
</li>
<li><p>API inventory management/ documentation</p>
</li>
<li><p>Resource provisioning using Infrastructure as Code</p>
</li>
</ol>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Security at every layer of an IT system is not negotiable. In this project, we’ve demonstrated how to leverage cloud-native solutions to secure APIs hosted in a serverless service, allowing only authorized users access to the APIs.</p>
<p>I’m Agnes Olorundare, and you can find out more about me on <a target="_blank" href="https://www.linkedin.com/in/agnes-olorundare-446055b8/"><strong>LinkedIn</strong></a><strong><em>.</em></strong></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Use Postman Scripts to Simplify Your API Authentication Process ]]>
                </title>
                <description>
                    <![CDATA[ Postman is a platform used by developers, API testers, technical writers and DevOps teams for testing, documenting and collaborating on API development. It provides a user-friendly interface for making different types of API requests (HTTP, GraphQL, ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-use-postman-scripts/</link>
                <guid isPermaLink="false">68bee731d2147595571c5b44</guid>
                
                    <category>
                        <![CDATA[ authentication ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ automation ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Orim Dominic Adah ]]>
                </dc:creator>
                <pubDate>Mon, 08 Sep 2025 14:24:49 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1757341168209/dc77dc00-a0a6-40f7-b766-ce07d0d8a637.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Postman is a platform used by developers, API testers, technical writers and DevOps teams for testing, documenting and collaborating on API development. It provides a user-friendly interface for making different types of API requests (HTTP, GraphQL, gRPC), inspecting responses, and organizing API calls into collections for collaboration and automation.</p>
<p>Performing repetitive tasks while testing APIs is stressful and time-wasting. For example, the process of retrieving, copying and pasting new authentication tokens for use in Postman is repetitive. You can simplify this process by using Postman scripts to store auth tokens and then reuse them without repeating the copy and paste steps.</p>
<p>To practice along with this guide, you should have:</p>
<ul>
<li><p>The <a target="_blank" href="https://www.postman.com/downloads/">Postman API client</a> installed on your computer</p>
</li>
<li><p>Experience in making API requests with Postman</p>
</li>
<li><p>A backend application that uses JWT authentication and has its documentation in your Postman client</p>
</li>
</ul>
<p>If you don’t have a backend application setup, I created one that you can clone from GitHub at <a target="_blank" href="https://github.com/orimdominic/freeCodeCamp-postman-api-jwt">orimdominic/freeCodeCamp-postman-api-jwt</a>.</p>
<p>By the end of this article, you should be able to simplify the process of obtaining and reusing authentication tokens across your API requests. You should also have a practical understanding of some scripts necessary for use in other areas of software testing with Postman.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-table-of-contents">Table of Contents</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-are-postman-scripts">What are Postman Scripts?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-simplify-your-jwt-authentication-process">How to Simplify Your JWT Authentication Process</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-authenticate-to-get-the-token">Authenticate to Get the Token</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-save-the-token-in-a-variable-with-a-postman-script">How to Save the Token in a Variable with a Postman Script</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-use-the-variable-in-a-request">How to Use the Variable in a Request</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-next-steps">Next Steps</a></p>
</li>
</ul>
<h2 id="heading-what-are-postman-scripts">What are Postman Scripts?</h2>
<p><a target="_blank" href="https://learning.postman.com/docs/tests-and-scripts/tests-and-scripts/">Postman scripts</a> are blocks of JavaScript code that you can write and run within the Postman API client to automate and enhance API testing workflows. You can use Postman scripts to add code to run before and after API requests. These scripts can be used to:</p>
<ul>
<li><p>Add logic and process data from API requests</p>
</li>
<li><p>Write test assertions for API responses</p>
</li>
<li><p>Run automated tests on API endpoints</p>
</li>
</ul>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756577771526/161bd327-fbf7-48cb-acab-317ab1cad4c5.jpeg" alt="The Postman scripts tab" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>You can find Postman scripts under the <strong>Scripts</strong> tab of an API request. Code written in the <strong>Pre-request</strong> tab runs before the request is made and code written in the <strong>Post-response</strong> tab runs after the response is made.</p>
<h2 id="heading-how-to-simplify-your-jwt-authentication-process">How to Simplify Your JWT Authentication Process</h2>
<p>In summary, you will carry out the following steps to achieve the objective of this tutorial:</p>
<ol>
<li><p>Authenticate to get the token</p>
</li>
<li><p>Save the token in a collection variable with Postman scripts</p>
</li>
<li><p>Use the variable in an API request</p>
</li>
</ol>
<h3 id="heading-authenticate-to-get-the-token">Authenticate to Get the Token</h3>
<p>To get started, carry out the following steps:</p>
<ol>
<li><p>Start your backend application and make sure it is running successfully.</p>
</li>
<li><p>Open up your Postman application and go to the API request for signing in to get a JWT.</p>
</li>
<li><p>Make an API request to the sign in endpoint and take note of the JSON response schema.</p>
</li>
</ol>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756573137191/b5aad14c-5094-4a84-8876-1bbbb869064c.jpeg" alt="Authentication request response" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>The highlighted part of the image above shows the JSON response from a successful sign in request. In the response schema, the auth token to be used for authorization is in the <code>data.token</code> field. You will use Postman scripts to store this token in a variable and then use the variable in the <code>Authorization</code> header of requests that require authorization.</p>
<h3 id="heading-how-to-save-the-token-in-a-variable-with-a-postman-script">How to Save the Token in a Variable with a Postman Script</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756575948975/2b43493d-2803-45cd-aefe-0ca5694f75e8.jpeg" alt="Add logic in Post-response Postman script" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>In Postman, click on the <strong>Scripts</strong> tab next to the <strong>Body</strong> tab. If the Postman application window is small, you may need to click a dropdown to see it. Next, click on the <strong>Post-response</strong> tab. In the text area to the right, you will write the script to capture the auth token from the response and store it in a Postman variable. Copy the JavaScript code below and paste it into the text area.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">if</span> (pm.response.code == <span class="hljs-number">200</span>) {
    <span class="hljs-keyword">const</span> token = pm.response.json().data.token
    pm.collectionVariables.set(<span class="hljs-string">"auth_token"</span>, token)
}
</code></pre>
<p>Postman scripts use the <a target="_blank" href="https://learning.postman.com/docs/tests-and-scripts/write-scripts/postman-sandbox-api-reference/"><code>pm</code> identifier</a> to access and modify information in the Postman environment. The script above uses <code>pm</code> to first ensure that the request was successful by checking if the response status code is <code>200</code>.</p>
<p>Inside the conditional statement, <code>pm.response.json().data.token</code> is used to get the authentication token from the JSON response and store it in a collection variable called <code>auth_token</code>. If <code>auth_token</code> doesn’t exist already, it is created and its value is set to the value of <code>token</code>. If it exists already, its value is replaced.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756581970294/bed1fe89-9c00-4b94-9f71-173ea3bf1cd1.png" alt="Postman collection variable set by a script" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>To confirm that <code>auth_token</code> has been set, click on the name of the collection (labelled 1 in the screenshot above) and then click on the <strong>Variables</strong> tab (labelled 2 in the screenshot above). Next, instead of repeatedly copying the token and pasting it in the <code>Authorization</code> header of your requests, you will use <code>auth_token</code> in the <code>Authorization</code> header of your requests.</p>
<h3 id="heading-how-to-use-the-variable-in-a-request">How to Use the Variable in a Request</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1756583915681/d3bf0f56-c406-4d3e-b7f1-df4cbc2a3cfc.png" alt="Use the Variable in a Request" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Reference the collection variable in the <code>Authorization</code> header by surrounding it with double curly braces <code>{{auth_token}}</code>. When you make an API request, Postman will use the value referenced by <code>{{auth_token}}</code> as the <code>Authorization</code> header.</p>
<p>If another authentication request causes the value of <code>auth_token</code> to be updated, you no longer need to copy the new auth token. The script in the post-response tab will update the <code>auth_token</code> value and you can go on with making API requests smoothly. No need for repeatedly copying and pasting - <strong>Don’t Repeat Yourself (DRY)</strong>.</p>
<h2 id="heading-next-steps">Next Steps</h2>
<p>In this tutorial, you have learnt how to use Postman scripts to set environment variables in Postman. You have also learnt how to eliminate the process of repeatedly copying and pasting auth tokens for use in API requests.</p>
<p>For guides on writing assertion tests for your APIs, check out the <a target="_blank" href="https://learning.postman.com/docs/tests-and-scripts/test-apis/test-apis/">Test API Functionality and Performance in Postman</a> guide by Postman.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Caching a Next.js API using Redis and Sevalla ]]>
                </title>
                <description>
                    <![CDATA[ When you hear about Next.js, your first thought may be static websites or React-driven frontends. But that’s just part of the story. Next.js can also power full-featured backend APIs that you can host and scale just like any other backend service. In... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/caching-a-nextjs-api-using-redis-and-sevalla/</link>
                <guid isPermaLink="false">68af2baaa845284174a27982</guid>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ caching ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Redis ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Wed, 27 Aug 2025 16:00:42 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1756310410998/ee5f34fd-0efe-4efc-9e91-2baa826edff9.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When you hear about Next.js, your first thought may be static websites or React-driven frontends. But that’s just part of the story. Next.js can also power full-featured backend APIs that you can host and scale just like any other backend service.</p>
<p><a target="_blank" href="https://www.freecodecamp.org/news/how-to-deploy-a-nextjs-api-with-postgresql-and-sevalla/">In an earlier article</a>, I walked through building a Next.js API and deploying it with Sevalla. The example stored data in a PostgreSQL database and handled requests directly. That worked fine, but as traffic grows, APIs that hit the database on every request can slow down.</p>
<p>This is where caching comes in. By adding Redis as a cache layer, we can make our Next.js API much faster and more efficient. In this article, we’ll see how to add Redis caching to our API, deploy it with <a target="_blank" href="https://sevalla.com/">Sevalla</a>, and show measurable improvements.</p>
<p>In the last article, I explained the API in detail. So you can <a target="_blank" href="https://github.com/manishmshiva/nextjs-api-pgsql">use this repository</a> to start with as the base for this project.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-why-caching-matters">Why Caching Matters</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-redis">What is Redis</a>?</p>
</li>
<li><p><a class="post-section-overview" href="#heading-setting-up-the-project">Setting Up the Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-provisioning-redis">Provisioning Redis</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-updating-cache-on-reads">Updating Cache on Reads</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-updating-cache-on-writes">Updating Cache on Writes</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-deploying-to-sevalla">Deploying to Sevalla</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-redis-works-well-with-nextjs-apis">Why Redis Works Well with Next.js APIs</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-why-caching-matters">Why Caching Matters</h2>
<p>Every time your API hits the database, it consumes time and resources. Databases are great at storing and querying structured data, but they aren’t optimized for speed at scale when you need to serve thousands of read requests per second.</p>
<p>Caching solves this by keeping frequently accessed data in memory. Instead of asking the database every time, the API can return data directly from cache if it’s available. Redis is perfect for this because it’s an in-memory key-value store designed for performance.</p>
<p>For example, if you fetch the list of users from the database on every request, it might take 200ms to run the query and return results. With Redis caching, the first request stores the result in memory, and subsequent requests can return the same data in less than 10ms. That’s an order-of-magnitude improvement.</p>
<h2 id="heading-what-is-redis">What is Redis?</h2>
<p><a target="_blank" href="https://www.freecodecamp.org/news/how-in-memory-caching-works-in-redis/">Redis</a> is an in-memory data store that works like a super-fast database. Instead of writing and reading from disk, it keeps data in memory, which makes it incredibly fast. That’s why it’s often used as a cache, where speed is more important than long-term storage.</p>
<p>It’s designed to handle high-throughput workloads with very low latency, which means it can respond in microseconds. This makes it a perfect fit for use cases like caching API responses, storing session data, or even powering real-time applications like chat systems and leaderboards.</p>
<p>Unlike a traditional database, Redis focuses on simplicity and speed. It stores data as key-value pairs, so you can quickly fetch or update values without writing complex queries. And because it supports advanced data types like lists, sets, and hashes, it’s much more flexible than a plain key-value store.</p>
<p>When combined with an API like the one we built in Next.js, Redis helps you reduce load on the main database and deliver blazing-fast responses to clients.</p>
<h2 id="heading-setting-up-the-project">Setting Up the Project</h2>
<p>Let’s clone the repository:</p>
<pre><code class="lang-typescript">git clone git<span class="hljs-meta">@github</span>.com:manishmshiva/nextjs-api-pgsql.git next-api
</code></pre>
<p>Now let’s go into the directory and do an npm install to install the packages.</p>
<pre><code class="lang-typescript">cd next-api
npm i
</code></pre>
<p>Create a .env file and add the database URL from Sevalla into an environment variable.</p>
<pre><code class="lang-typescript">cat .env
</code></pre>
<p>The .env file should look like this:</p>
<pre><code class="lang-typescript">PGSQL_URL=postgres:<span class="hljs-comment">//&lt;username&gt;:&lt;password&gt;-@asia-east1-001.proxy.kinsta.app:30503/&lt;db_name&gt;</span>
</code></pre>
<p>Now let’s make sure the application works as expected by starting the application and making a couple of API requests.</p>
<p>Starting the app:</p>
<pre><code class="lang-typescript">npm run dev
</code></pre>
<p>Let’s make sure the database is connected. Go to <code>localhost:3000</code> on your browser. It should return the following JSON:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755607650708/543df6fe-3bea-4eb2-b962-13df35b6fb2c.png" alt="GET /" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Let’s create a new user. To create a new entry in the DB using <a target="_blank" href="https://www.postman.com/">Postman</a>, send a POST request with the following JSON:</p>
<pre><code class="lang-typescript">{<span class="hljs-string">"id"</span>:<span class="hljs-string">"d9553bb7-2c72-4d92-876b-9c3b40a8c62c"</span>,<span class="hljs-string">"name"</span>:<span class="hljs-string">"Larry"</span>,<span class="hljs-string">"email"</span>:<span class="hljs-string">"larry@example.com"</span>,<span class="hljs-string">"age"</span>:<span class="hljs-string">"25"</span>}
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755607596858/7c8c71b5-7868-47a1-ae8d-172474f6d75b.png" alt="Postman test" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Let’s ensure the record is created by going to <code>localhost:3000/users</code> in the browser.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755607717319/d6743d2a-8373-4d81-afee-1f034e1954e1.png" alt="Postman response for /users" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Great. Now let’s cache these APIs using Redis.</p>
<h2 id="heading-provisioning-redis">Provisioning Redis</h2>
<p>Let’s go to <a target="_blank" href="https://app.sevalla.com/login">Sevalla’s</a> dashboard and click on “Databases”. Choose “Redis” from the list, and leave the rest of the options as defaults.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755415913475/0ba7badb-2c67-474a-a5b1-6d72b5bdc5f3.png" alt="Create database" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Once the database is created, switch on the “external connection” option and copy the publicly accessible URL.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755611728877/6e139e09-8484-4a50-b007-32ecdb266afb.png" alt="Update network settings" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>This is how it should look in the .env file:</p>
<pre><code class="lang-typescript">REDIS_URL=redis:<span class="hljs-comment">//default:&lt;password&gt;@&lt;host&gt;:&lt;port&gt;</span>
</code></pre>
<p>Now install a Redis client for Node.js:</p>
<pre><code class="lang-bash">npm install ioredis
</code></pre>
<p>We can now connect to Redis and use it as a cache layer for our users API. Let’s see how to implement caching.</p>
<h2 id="heading-updating-cache-on-reads">Updating Cache on Reads</h2>
<p>Here’s the updated <code>users/route.ts</code> that uses Redis:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">import</span> { NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;
<span class="hljs-keyword">import</span> { Client } <span class="hljs-keyword">from</span> <span class="hljs-string">"pg"</span>;
<span class="hljs-keyword">import</span> Redis <span class="hljs-keyword">from</span> <span class="hljs-string">"ioredis"</span>;

<span class="hljs-keyword">const</span> redis = <span class="hljs-keyword">new</span> Redis(process.env.REDIS_URL!);

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">readUsers</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> Client({
    connectionString: process.env.PGSQL_URL,
  });
  <span class="hljs-keyword">await</span> client.connect();

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> client.query(<span class="hljs-string">"SELECT id, name, email, age FROM users"</span>);
    <span class="hljs-keyword">return</span> result.rows;
  } <span class="hljs-keyword">finally</span> {
    <span class="hljs-keyword">await</span> client.end();
  }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GET</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-comment">// Check cache first</span>
    <span class="hljs-keyword">const</span> cached = <span class="hljs-keyword">await</span> redis.get(<span class="hljs-string">"users"</span>);
    <span class="hljs-keyword">if</span> (cached) {
      <span class="hljs-keyword">return</span> NextResponse.json(<span class="hljs-built_in">JSON</span>.parse(cached));
    }

    <span class="hljs-comment">// Fallback to database if not cached</span>
    <span class="hljs-keyword">const</span> users = <span class="hljs-keyword">await</span> readUsers();

    <span class="hljs-comment">// Store result in cache with 60s TTL</span>
    <span class="hljs-keyword">await</span> redis.set(<span class="hljs-string">"users"</span>, <span class="hljs-built_in">JSON</span>.stringify(users), <span class="hljs-string">"EX"</span>, <span class="hljs-number">60</span>);

    <span class="hljs-keyword">return</span> NextResponse.json(users);
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-keyword">return</span> NextResponse.json({ error: <span class="hljs-string">"Failed to fetch users"</span> }, { status: <span class="hljs-number">500</span> });
  }
}
</code></pre>
<p>Now, when you hit <code>/users</code>:</p>
<ol>
<li><p>The API first checks Redis.</p>
</li>
<li><p>If the data exists, it returns it instantly.</p>
</li>
<li><p>If not, it queries PostgreSQL, saves the result in Redis, and then returns it.</p>
</li>
</ol>
<p>This makes repeated requests extremely fast. You can adjust the cache expiry (<code>EX 60</code>) depending on how fresh your data needs to be.</p>
<p>Without Redis caching, fetching <code>/users</code> ten times means ten database queries. Each might take around 150–200ms depending on database size and network latency.</p>
<p>With Redis, the first request still takes ~200ms since it populates the cache. But every request after that is nearly instant, often under 10ms. That’s a <strong>20x improvement</strong>.</p>
<p>This speedup matters when your API faces hundreds or thousands of requests per second. Caching not only reduces latency but also lightens the load on your database.</p>
<h2 id="heading-updating-cache-on-writes">Updating Cache on Writes</h2>
<p>Right now, only GET requests use the cache. But what if we add new users? The cache would still return the old data.</p>
<p>The solution is to update or clear the cache whenever a write happens. Let’s update the <code>POST</code> handler:</p>
<pre><code class="lang-ts"><span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">POST</span>(<span class="hljs-params">req: Request</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> body = <span class="hljs-keyword">await</span> req.json();
    <span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> Client({
      connectionString: process.env.PGSQL_URL,
    });
    <span class="hljs-keyword">await</span> client.connect();

    <span class="hljs-keyword">const</span> query = <span class="hljs-string">`
      INSERT INTO users (id, name, email, age)
      VALUES ($1, $2, $3, $4)
      RETURNING *;
    `</span>;

    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> client.query(query, [
      body.id,
      body.name,
      body.email,
      body.age,
    ]);

    <span class="hljs-keyword">await</span> client.end();

    <span class="hljs-comment">// Invalidate cache so next GET fetches fresh data</span>
    <span class="hljs-keyword">await</span> redis.del(<span class="hljs-string">"users"</span>);

    <span class="hljs-keyword">return</span> NextResponse.json(result.rows[<span class="hljs-number">0</span>]);
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-keyword">return</span> NextResponse.json({ error: <span class="hljs-string">"Failed to add user"</span> }, { status: <span class="hljs-number">500</span> });
  }
}
</code></pre>
<p>Now whenever a new user is created, the cache for <code>users</code> is cleared. The next GET request will fetch from the database, refresh the cache, and then continue serving cached data.</p>
<h2 id="heading-deploying-to-sevalla"><strong>Deploying to Sevalla</strong></h2>
<p>Push your code to GitHub or <a target="_blank" href="https://github.com/manishmshiva/nextjs-api-redis">fork my repository</a>. Now lets go to Sevalla and create a new app.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754545093624/9747a06d-0dcf-482a-89b9-732b9937b1dc.png" alt="Sevalla create app" width="600" height="400" loading="lazy"></p>
<p>Choose your repository from the dropdown and check “Automatic deployment on commit”. This will ensure that the deployment is automatic every time you push code. Choose “Hobby” under the resources section.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755683871627/cc8bd555-caaa-43f2-a9a3-f3f0d1481108.png" alt="create new app" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Click “Create” and not “Create and deploy”. We haven’t added our PostgreSQL URL and Redis URL as environment variables, so the app will crash if you try to deploy it.</p>
<p>Go to the “Environment variables” section and add the key “PGSQL_URL” and the URL in the value field. Do the same for the “REDIS_URL” key and add the Redis URL.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755683943610/97932885-29aa-4cef-b999-689f0871809e.png" alt="Adding environment variables" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Now go back to the “Overview” section and click “Deploy now”.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1755684196447/a1da5802-aa2c-47f6-8837-14f7e40198fd.png" alt="App Deployment" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Once deployment is complete, click “Visit app” to get the live URL of your API. You can replace <a target="_blank" href="http://localhost:3000">localhost:3000</a> with the new URL in Postman and test your API.</p>
<h2 id="heading-why-redis-works-well-with-nextjs-apis">Why Redis Works Well with Next.js APIs</h2>
<p>Redis is lightweight, blazing fast, and perfect for caching API responses. In the context of Next.js, it fits naturally because:</p>
<ul>
<li><p>The API routes run server-side where Redis can be queried directly.</p>
</li>
<li><p>Caching logic is simple to add around database calls.</p>
</li>
<li><p>Redis can be used for more than caching – things like rate limiting, session storage, and pub/sub are also common patterns.</p>
</li>
</ul>
<p>By combining Next.js, PostgreSQL, and Redis on Sevalla, you get a stack that is fast, scalable, and easy to deploy.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Caching isn’t just an optimization – it’s a necessity for real-world APIs. Next.js helps you build robust backend APIs that can be deployed easily. By adding Redis to the mix, those APIs can handle scale without breaking a sweat.</p>
<p>Sevalla ties it all together by providing managed PostgreSQL, Redis, and app hosting in one place. With a few environment variables and a GitHub repo, you can go from local dev to a production-ready, cached API in minutes.</p>
<p><em>Hope you enjoyed this article. Signup for my free AI newsletter</em> <a target="_blank" href="https://www.turingtalks.ai/"><strong><em>TuringTalks.ai</em></strong></a> <em>for more hands-on tutorials on AI. You can also find me on</em> <a target="_blank" href="https://www.linkedin.com/in/manishmshiva"><strong><em>Linkedin</em></strong></a><strong><em>.</em></strong></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Deploy a Next.js API with PostgreSQL and Sevalla ]]>
                </title>
                <description>
                    <![CDATA[ When developers think of Next.js, they often associate it with SEO-friendly static websites or React-based frontends. But what many miss is how Next.js can also be used to build full-featured backend APIs – all within the same project. I’ve recently ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-deploy-a-nextjs-api-with-postgresql-and-sevalla/</link>
                <guid isPermaLink="false">68a33084f6c19271552e2ab0</guid>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ PostgreSQL ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Manish Shivanandhan ]]>
                </dc:creator>
                <pubDate>Mon, 18 Aug 2025 13:54:12 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1755525213723/75759868-d5e9-4ea7-a6be-22bc33dde0d8.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>When developers think of Next.js, they often associate it with SEO-friendly static websites or React-based frontends. But what many miss is how Next.js can also be used to build full-featured backend APIs – all within the same project.</p>
<p>I’ve <a target="_blank" href="https://www.freecodecamp.org/news/how-to-deploy-a-nextjs-api-to-production-using-sevalla/">recently written an article</a> on working with Next.js API and deploying it to production. In this case, I would’ve used a JSON file as a mini-database.</p>
<p>But JSON or any type of file storage isn’t fit for a production application. This is because file-based storage isn’t designed for concurrent access, so multiple users writing data at the same time can cause corruption or loss.</p>
<p>It also lacks indexing and query capabilities, making it slow as data grows. Backups, security, and scalability are also harder to manage compared to a proper database.</p>
<p>In short, while JSON files work for demos or prototypes, production systems need a database that can handle concurrency, large datasets, complex queries, and reliable persistence.</p>
<p>So in this article, we'll walk through how to build a REST API with Next.js, store data in a Sevalla-managed database, and deploy the whole project to production using Sevalla's <a target="_blank" href="https://www.freecodecamp.org/news/vps-vs-paas-how-to-choose-a-hosting-solution/">PaaS infrastructure</a>.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-nextjs">What is Next.js?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-installation-and-setup">Installation and Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-build-a-nextjs-api">How to Build a NextJS API</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-provisioning-a-database-in-sevalla">Provisioning a Database in Sevalla</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-deploying-to-sevalla">Deploying to Sevalla</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-nextjs"><strong>What is Next.js?</strong></h2>
<p><a target="_blank" href="https://nextjs.org/">Next.js</a> is an open-source React framework developed by Vercel. It's known for server-side rendering, static generation, and seamless routing. But beyond its frontend superpowers, it allows developers to build backend logic and APIs through its file-based routing system. This makes Next.js a great choice for building full-stack apps.</p>
<h2 id="heading-installation-and-setup"><strong>Installation and Setup</strong></h2>
<p>To get started, make sure Node.js and NPM are installed.</p>
<pre><code class="lang-bash">$ node --version
v22.16.0

$ npm --version
10.9.2
</code></pre>
<p>Now, create a new Next.js project:</p>
<pre><code class="lang-bash">npx create-next-app@latest
</code></pre>
<p>The result of the above command will ask you a series of questions to setup your app:</p>
<pre><code class="lang-plaintext">What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like your code inside a `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to use Turbopack for `next dev`?  No / Yes
Would you like to customize the import alias (`@/*` by default)? No / Yes
What import alias would you like configured? @/*
</code></pre>
<p>But for this tutorial, we aren’t interested in a full stack app – just an API. So let’s re-create the app using the <code>— - api</code> flag.</p>
<pre><code class="lang-plaintext">$ npx create-next-app@latest --api
</code></pre>
<p>It will still ask you a few questions. Use the default settings and finish creating the app.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754476744959/9f1d2763-7df5-491b-8cb3-05161b35fbd9.webp" alt="9f1d2763-7df5-491b-8cb3-05161b35fbd9" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Once the setup is done, you can see the folder with your app name. Let’s go into the folder and run the app.</p>
<pre><code class="lang-plaintext">$ npm run dev
</code></pre>
<p>Your API template should be running at port 3000. Go to <a target="_blank" href="http://localhost:3000/">http://localhost:3000</a> and you should see the following message:</p>
<pre><code class="lang-plaintext">{
"message": "Hello world!"
}
</code></pre>
<h2 id="heading-how-to-build-a-nextjs-api"><strong>How to Build a NextJS API</strong></h2>
<p>Now that we’ve set up our API template, let's write a basic REST API with two endpoints: one to create data and one to view data</p>
<p>The API code will reside under /app within the project directory. Next.js uses file-based routing for building URL paths.</p>
<p>For example, if you want a URL path /users, you should have a directory called “users” with a route.ts file to handle all the CRUD operations for /users. For /users/:id, you should have a directory called [id] under “users” directory with a route.ts file. The square brackets are to tell Next.js that you expect dynamic values for the /users/:id route.</p>
<p>Here is a screenshot of the setup. Delete the [slug] directory that comes with the project since it won’t be relevant for us.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754479396056/a80a0fd3-707d-4813-b402-041561354c94.png" alt="Folder setup" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<ul>
<li><p>The route.ts file at the bottom handles CRUD operations for / (this is where the response “hello world” was generated from)</p>
</li>
<li><p>The route.ts file under /users handles CRUD operations for /users</p>
</li>
</ul>
<p>While this setup can seem complicated for a simple project, it provides a clear structure for large-scale web applications. If you want to go deeper into building complex APIs with Next.js, <a target="_blank" href="https://nextjs.org/blog/building-apis-with-nextjs">here is a tutorial</a> you can follow.</p>
<p>The code under /app/route.ts is the default file for our API. You can see it serving the GET request and responding with “Hello World!”:</p>
<pre><code class="lang-plaintext">import { NextResponse } from "next/server";

export async function GET() {
  return NextResponse.json({ message: "Hello world!" });
}
</code></pre>
<p>Now we need two routes:</p>
<ul>
<li><p>GET /users which lists all users</p>
</li>
<li><p>POST /users which creates a new user</p>
</li>
</ul>
<p>For this project, we’ll use a database to store our records. We’re not going to install a database on our local machine. Instead, we’ll provision the database in the cloud and use it with our API. This approach is common in test / prod environments to ensure data consistency.</p>
<h2 id="heading-provisioning-a-database-in-sevalla">Provisioning a Database in Sevalla</h2>
<p><a target="_blank" href="https://sevalla.com/">Sevalla</a> is a modern, usage-based Platform-as-a-service provider and an alternative to sites like Heroku or to your self-managed setup on AWS. It combines powerful features with a smooth developer experience.</p>
<p>Sevalls offers application hosting, database, object storage, and static site hosting for your projects. It comes with a generous free tier, so we’ll use it to connect to a database as well as deploy our app to the cloud.</p>
<p>If you are new to Sevalla, you can <a target="_blank" href="https://sevalla.com/signup/">sign up</a> using your GitHub account to enable direct deploys from your GitHub. Every time you push code to your project, Sevalla will auto-pull and deploy your app to the cloud.</p>
<p>Once you login to Sevalla, click on “Databases”.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754477578430/7b7fa655-0f35-4901-90be-07bd5abdf2c0.png" alt="Sevalla Databases" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Now let’s create a <a target="_blank" href="https://www.freecodecamp.org/news/posgresql-course-for-beginners/">PostgreSQL</a> database.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754477639118/d6ea82ae-45c9-40a7-bcf5-d144885db929.png" alt="Create Postgresql Database" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Use the default settings. Once the database is created, it will disable the external connections by default for security to ensure no one outside our server can connect to it. Since we want to test our connection from our local machine, let’s enable an external connection.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754479205197/58c01504-59c0-4df3-b9f9-cb14e1431135.png" alt="Database settings" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>The value we need to connect to the database from our local endpoint is “url” under external connection. Create a file called .env in the project and paste the URL in the below format:</p>
<pre><code class="lang-typescript">PGSQL_URL=postgres:<span class="hljs-comment">//&lt;username&gt;:&lt;password&gt;-@asia-east1-001.proxy.kinsta.app:30503/&lt;db_name&gt;</span>
</code></pre>
<p>The reason we use .env is to store environment variables specific to the environment. In production, we won’t need this file (never push .env files to GitHub). Sevalla will give us the option to add environment variables via the GUI when we deploy the app.</p>
<p>Now let’s test our database connection. Install the <code>pg</code> package for Node to interact with PostgreSQL. Let’s also install the TypeScript extension for <code>pg</code> to support TypeScript definitions.</p>
<pre><code class="lang-typescript">$ npm i pg
$ npm install --save-dev <span class="hljs-meta">@types</span>/pg
</code></pre>
<p>Change the route.ts that served “hello world” to the below:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// app/api/your-endpoint/route.ts</span>
<span class="hljs-keyword">import</span> { NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;
<span class="hljs-keyword">import</span> { Client } <span class="hljs-keyword">from</span> <span class="hljs-string">"pg"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GET</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> client = <span class="hljs-keyword">new</span> Client({
    connectionString: process.env.PGSQL_URL,
  });

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> client.connect();
    <span class="hljs-keyword">await</span> client.end();
    <span class="hljs-keyword">return</span> NextResponse.json({ message: <span class="hljs-string">"Connected to database"</span> });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Database connection error:"</span>, error);
    <span class="hljs-keyword">return</span> NextResponse.json({ message: <span class="hljs-string">"Connection failed"</span> }, { status: <span class="hljs-number">500</span> });
  }
}
</code></pre>
<p>Now when your app and go to localhost:3000, it should say “connected to database”.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754485714515/c63f11fc-2310-462a-9b42-c0528e500637.png" alt="Postgresql successful connection" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Great. Now let’s write our two routes, one to create data and the other to view the data we created. Use this code under users/route.ts:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { NextRequest } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/server"</span>;
<span class="hljs-keyword">import</span> { Client } <span class="hljs-keyword">from</span> <span class="hljs-string">"pg"</span>;

<span class="hljs-comment">// Define the structure of a User object</span>
<span class="hljs-keyword">interface</span> User {
  id: <span class="hljs-built_in">string</span>;
  name: <span class="hljs-built_in">string</span>;
  email: <span class="hljs-built_in">string</span>;
  age: <span class="hljs-built_in">number</span>;
}

<span class="hljs-comment">// Create a PostgreSQL client</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getClient</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Client({
    connectionString: process.env.PGSQL_URL,
  });
}

<span class="hljs-comment">// Fetch all users from the database</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">readUsers</span>(<span class="hljs-params"></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">User</span>[]&gt; </span>{
  <span class="hljs-keyword">const</span> client = getClient();
  <span class="hljs-keyword">await</span> client.connect();

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> client.query(<span class="hljs-string">"SELECT id, name, email, age FROM users"</span>);
    <span class="hljs-keyword">return</span> result.rows;
  } <span class="hljs-keyword">finally</span> {
    <span class="hljs-keyword">await</span> client.end();
  }
}

<span class="hljs-comment">// Insert or update users in the database</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">writeUsers</span>(<span class="hljs-params">users: User[]</span>) </span>{
  <span class="hljs-keyword">const</span> client = getClient();
  <span class="hljs-keyword">await</span> client.connect();

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> insertQuery = <span class="hljs-string">`
      INSERT INTO users (id, name, email, age)
      VALUES ($1, $2, $3, $4)
      ON CONFLICT (id) DO UPDATE SET
        name = EXCLUDED.name,
        email = EXCLUDED.email,
        age = EXCLUDED.age;
    `</span>;

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> user <span class="hljs-keyword">of</span> users) {
      <span class="hljs-keyword">await</span> client.query(insertQuery, [user.id, user.name, user.email, user.age]);
    }
  } <span class="hljs-keyword">finally</span> {
    <span class="hljs-keyword">await</span> client.end();
  }
}

<span class="hljs-comment">// Handle GET request: return list of users</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">GET</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> users = <span class="hljs-keyword">await</span> readUsers();
    <span class="hljs-keyword">return</span> NextResponse.json(users);
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error reading users from DB:"</span>, err);
    <span class="hljs-keyword">return</span> NextResponse.json({ error: <span class="hljs-string">"Failed to fetch users"</span> }, { status: <span class="hljs-number">500</span> });
  }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">POST</span>(<span class="hljs-params">req: NextRequest</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> body = <span class="hljs-keyword">await</span> req.json();
    <span class="hljs-keyword">const</span> users: User[] = <span class="hljs-built_in">Array</span>.isArray(body) ? body : [body];

    <span class="hljs-keyword">await</span> writeUsers(users);

    <span class="hljs-keyword">return</span> NextResponse.json({ success: <span class="hljs-literal">true</span>, count: users.length });
  } <span class="hljs-keyword">catch</span> (err) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">"Error writing users to DB:"</span>, err);
    <span class="hljs-keyword">return</span> NextResponse.json({ error: <span class="hljs-string">"Failed to write users"</span> }, { status: <span class="hljs-number">500</span> });
  }
}
</code></pre>
<p>Now when you go to localhost:3000/users, it will give you an error because the users table does exist. So let’s create one.</p>
<p>In the database UI, click on “Studio”. You’ll get a visual editor for your database where you can manage your data directly (pretty cool, right?).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754486852876/2437c76a-562a-4575-9cc0-a5f563aa6206.png" alt="Database studio" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Press the “+” icon and choose “create table”. Create the table with the schema below. Click the “add column” link to create new columns.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754487097219/9d01d9b7-e3c6-427b-9b42-c97065826af7.png" alt="Database Schema" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Click “create table and you should see the table created as below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754487162119/c64e0577-d094-4549-85f4-ab8c8d15f48e.png" alt="Users table" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Let’s add a dummy record using “add record” button to use it to test our API. The id field should be in UUID format (and you can <a target="_blank" href="https://www.uuidgenerator.net/">generate one here</a>).</p>
<p>Now let’s test our API.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754487408705/3fd9784e-3a83-415d-870f-f3f5d23dec51.png" alt="3fd9784e-3a83-415d-870f-f3f5d23dec51" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>You should see the user you created as the response to the localhost:3000/users query. Now let’s create a new user using our API.</p>
<p>We’ll use <a target="_blank" href="https://www.postman.com/">Postman</a> for this since its easy to create POST requests using Postman. We’ll send a sample data under “body” → “raw” → “JSON”.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754543941765/a77be1b8-05c3-4c61-a5c3-f7f0fbf48b4d.png" alt="Post Request" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>The response from Postman should be as below:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754544001954/5a52331c-a445-4b10-8c4b-9337ca873c13.png" alt="Postman results" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Now going to localhost:3000/users, you should see the new record created.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754544086690/8c4533c6-4250-42e1-a850-b52c460775fc.png" alt="Get /users" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Great job! Now let’s get this app live.</p>
<h2 id="heading-deploying-to-sevalla">Deploying to Sevalla</h2>
<p>Push your code to GitHub or <a target="_blank" href="https://github.com/manishmshiva/nextjs-api-pgsql">fork my repository</a>. Now lets go to Sevalla and create a new app.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754545093624/9747a06d-0dcf-482a-89b9-732b9937b1dc.png" alt="Sevalla create app" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Choose your repository from the dropdown and check “Automatic deployment on commit”. This will ensure that the deployment is automatic every time you push code. Choose “Hobby” under the resources section.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754545136001/dde5fe4d-4691-401c-a3ef-959b8e53f62a.png" alt="Sevalla Create New App" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Click “Create” and not “Create and deploy”. We haven’t added our PostgreSQL URL as an environment variable, so the app will crash if you try to deploy it.</p>
<p>Go to the “Environment variables” section and add the key “PGSQL_URL” and the URL in the value field.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754545371348/7525c6cd-63af-40b2-80c5-6b49b6101f19.png" alt="7525c6cd-63af-40b2-80c5-6b49b6101f19" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Now go back to the “Overview” section and click “Deploy now”.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1754545664510/c3f12f86-0732-4518-bf51-4867ac86abdd.png" alt="c3f12f86-0732-4518-bf51-4867ac86abdd" class="image--center mx-auto" width="600" height="400" loading="lazy"></p>
<p>Once deployment is complete, click “Visit app” to get the live URL of your API. You can replace localhost:3000 with the new URL in Postman and test your API.</p>
<p>Congratulations – your app is now live. You can do more with your app using the admin interface, like:</p>
<ul>
<li><p>Monitor the performance of your app</p>
</li>
<li><p>Watch real-time logs</p>
</li>
<li><p>Add custom domains</p>
</li>
<li><p>Update network settings (open/close ports for security, and so on)</p>
</li>
<li><p>Add more storage</p>
</li>
</ul>
<h2 id="heading-conclusion"><strong>Conclusion</strong></h2>
<p>Next.js is no longer just a frontend framework. It’s a powerful full-stack platform that lets you build and deploy production-ready APIs with minimal friction. By pairing it with Sevalla’s developer-friendly infrastructure, you can go from local development to a live, cloud-hosted API in minutes.</p>
<p>In this tutorial, you learned how to set up a Next.js API project, connect it to a cloud-hosted PostgreSQL database on Sevalla, and deploy everything seamlessly. Whether you're building a small side project or a full-scale application, this stack gives you the speed, structure, and scalability to move fast without losing flexibility.</p>
<p>Hope you enjoyed this article. I’ll see you soon with another one. You can <a target="_blank" href="https://manishshivanandhan.com/">connect with me here</a> or <a target="_blank" href="https://blog.manishshivanandhan.com/">visit my blog</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Fetch API Data in React Using Axios ]]>
                </title>
                <description>
                    <![CDATA[ Learning how to fetch data from APIs is a must-have skill for any developer. Whether you're building a simple portfolio site or working on real-world applications, you'll often need to connect to external data sources. Being comfortable with API call... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-fetch-api-data-in-react-using-axios/</link>
                <guid isPermaLink="false">6864461e87c1e49678c8da93</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Frontend Development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ axios in react ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Oluwadamisi Samuel ]]>
                </dc:creator>
                <pubDate>Tue, 01 Jul 2025 20:33:34 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1751385483454/7e7949aa-4bcd-4f58-9725-36df67b866a5.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Learning how to <code>fetch</code> data from <code>APIs</code> is a must-have skill for any developer. Whether you're building a simple portfolio site or working on real-world applications, you'll often need to connect to external data sources. Being comfortable with API calls shows you're ready to contribute to real projects and work well in a team.</p>
<p>This beginner-friendly tutorial is designed for junior developers and anyone new to React. You'll learn how to <code>fetch data</code> from an API, then <code>store</code> and <code>display</code> it in your React app. No advanced knowledge required – we'll break everything down step by step, so you can follow along and build confidence as you go.</p>
<p>We'll be using <code>React</code>, <code>Vite</code>, <code>Axios</code>, and <code>Tailwind CSS</code> to build a simple app that retrieves and displays data from a public API. First, we’ll fetch data using the built-in fetch method. Then we’ll refactor it using Axios, a popular library that simplifies <code>HTTP requests</code>.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along with this article, you should:</p>
<ul>
<li><p>Be familiar with basic React concepts like components and <code>useState</code></p>
</li>
<li><p>Know what an <code>API</code> is and that it returns data (usually in JSON)</p>
</li>
<li><p>Have some experience with JavaScript promises and the <code>.then()</code> method. (If you’ve seen or used <code>.then()</code> before, that’s enough – we'll build on that).</p>
</li>
<li><p>Be comfortable using <code>map()</code> to render lists from arrays (the data we get from the API)</p>
</li>
<li><p>Be able to run a React project using tools like Vite or Create React App</p>
</li>
</ul>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-what-is-an-api-and-why-do-we-need-it">What is an API and Why Do We Need it?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-types-of-apis-youll-encounter">Types of APIs You’ll Encounter</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-tools-well-use">Tools We’ll Use</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-fetch-data-with-react">How to Fetch Data with React</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-how-to-fetch-data-with-fetch">How to Fetch Data with fetch()</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-refactor-with-axios">Refactor with Axios</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-handle-loading-and-error-states">How to Handle Loading and Error States</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-is-a-loading-state">What is a Loading State?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-what-is-an-error-state">What is an Error State?</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-how-to-keep-your-api-keys-safe">How to Keep Your API Keys Safe</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-why-its-important">Why it's Important:</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-fun-public-apis-to-practice-with">Fun Public APIs to Practice With</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ol>
<h2 id="heading-what-is-an-api-and-why-do-we-need-it">What is an API and Why Do We Need it?</h2>
<p>An API, or Application Programming Interface, is a way for different systems to communicate. Think of it like a waiter at a restaurant. You tell the waiter what you want from the menu (the request), they take that order to the kitchen (the server), and then bring your food back to the table (the response).</p>
<p>In web development, APIs let your frontend application talk to a backend service. Most of the time, this communication happens through HTTP requests. You make a request to a specific URL (called an endpoint), and you get a response, usually in <code>JSON (JavaScript Object Notation)</code> format. JSON is lightweight, easy to read, and works well with JavaScript.</p>
<p>Here’s a basic GET request example:</p>
<pre><code class="lang-javascript">GET https:<span class="hljs-comment">//jsonplaceholder.typicode.com/users</span>
</code></pre>
<p>This GET request asks the server for a list of users. The response will look something like this:</p>
<pre><code class="lang-javascript">[
  {
    <span class="hljs-string">"id"</span>: <span class="hljs-number">1</span>,
    <span class="hljs-string">"name"</span>: <span class="hljs-string">"John Doe"</span>,
    <span class="hljs-string">"email"</span>: <span class="hljs-string">"JohnDOe@email.com"</span>,
  },
  {
    <span class="hljs-string">"id"</span>: <span class="hljs-number">2</span>,
    <span class="hljs-string">"name"</span>: <span class="hljs-string">"Jane Doe"</span>,
    <span class="hljs-string">"email"</span>: <span class="hljs-string">"JaneDoe@email.com"</span>,
  },
   <span class="hljs-comment">//...more users</span>
]
</code></pre>
<p>Your React app can grab this JSON, store it in state, and display it in the browser. That’s the basic API cycle you’ll see again and again in real-world applications:</p>
<ul>
<li><p>Make the request</p>
</li>
<li><p>Wait for the response</p>
</li>
<li><p>Parse the JSON</p>
</li>
<li><p>Use the data in your UI</p>
</li>
</ul>
<p>Understanding <code>APIs</code> and <code>JSON</code> is essential. You’ll use them to fetch user profiles, submit forms, update dashboards, search databases, and so much more.</p>
<h2 id="heading-types-of-apis-youll-encounter">Types of APIs You’ll Encounter</h2>
<p>Not all APIs are the same. Understanding the types of APIs you'll come across will help you know what tools or steps you’ll need to work with them.</p>
<h3 id="heading-1-public-apis-no-key-required">1. Public APIs (No Key Required)</h3>
<p>These are open-access APIs that anyone can use. They don’t require authentication or an API key. They're great for testing, learning, and building demo apps.</p>
<p>Example:</p>
<p><code>GET https://jsonplaceholder.typicode.com/users</code></p>
<h3 id="heading-2-public-apis-with-api-key">2. Public APIs (With API Key)</h3>
<p>Some APIs are public, but still require an API key. This helps the provider track usage and prevent abuse. You'll usually sign up to get a free key.</p>
<p>Example:</p>
<p><code>GET https://newsapi.org/v2/top-headlines?country=us&amp;apiKey=YOUR_API_KEY</code></p>
<ul>
<li><p><code>https://newsapi.org/v2/top-headlines</code> – the actual API endpoint</p>
</li>
<li><p><code>country=us</code> – a query parameter specifying you want “US headlines”</p>
</li>
<li><p><code>apiKey=YOUR_API_KEY</code> – this is your personal API key you get after signing up on <a target="_blank" href="http://newsapi.org">newsapi.org</a></p>
</li>
</ul>
<p><strong>To use these APIs, you’ll need to:</strong></p>
<ul>
<li><p>Sign up on the provider’s site</p>
</li>
<li><p>Store your key (safely) in your app (we will explore that later on)</p>
</li>
<li><p>Pass it as a query parameter or header</p>
</li>
</ul>
<h3 id="heading-3-private-apis">3. Private APIs</h3>
<p>These are usually used internally in companies. They often require more advanced forms of authentication, like OAuth tokens or session cookies. You won’t typically use these unless you’re working on the backend or within a team project.</p>
<h3 id="heading-4-using-bearer-tokens-for-api-authentication">4. Using Bearer Tokens for API Authentication</h3>
<p>When working with modern APIs, it's common to encounter APIs that require authentication using a <code>Bearer token</code> instead of a simple API key in the URL. The only difference here is that you pass in an <code>object</code> that contains the Bearer token instead of just the api key variable (for example, The Movie Database (TMDB) API).</p>
<p>This approach is more secure because it keeps the token out of the URL and browser history. It also aligns with token-based authentication standards like OAuth 2.0. </p>
<p><strong>Note:</strong> When working with third-party APIs, always check the documentation to see how authentication should be handled. Authentication methods vary – some APIs require passing the key in the URL, others expect it in the headers.</p>
<h2 id="heading-tools-well-use">Tools We’ll Use</h2>
<ul>
<li><p><strong>React:</strong> our JavaScript UI library of choice</p>
</li>
<li><p><strong>Tailwind CSS:</strong> For quick styling</p>
</li>
<li><p><strong>fetch:</strong> Native browser method for making HTTP requests</p>
</li>
<li><p><strong>Axios:</strong> Optional library that makes requests more convenient</p>
</li>
</ul>
<p>Learning how to use these tools and methods will make it easier to adapt to different production methods and environments.</p>
<h2 id="heading-how-to-fetch-data-with-react">How to Fetch Data with React</h2>
<p>Now you need to understand the basic structure and tools you need in order to fetch data with React and store that data to use in your components. To do this properly, you'll need to understand a few core tools and concepts:</p>
<ul>
<li><p><strong>useState hook:</strong> Lets you create and manage local state inside your component. You'll use it to hold the data you fetch, and track things like whether you're still loading or if there was an error.</p>
</li>
<li><p><strong>useEffect hook:</strong> Allows you to perform operations that need to run after the component renders, such as fetching data, subscribing to events, or updating the DOM.</p>
</li>
<li><p><strong>HTTP Requests:</strong> These are how you talk to APIs. You can use the browser-native fetch() method or third-party tools like Axios.</p>
</li>
</ul>
<p>A basic data fetching flow looks like this:</p>
<ul>
<li><p>Set up state to hold your data when it arrives </p>
</li>
<li><p>Use the useEffect() hook to make the API call</p>
</li>
<li><p>Handle loading and error states</p>
</li>
<li><p>Store and display the data once it arrives</p>
</li>
</ul>
<p>Now that you’ve got the fundamentals, let’s walk through two ways to fetch data: first using <code>fetch()</code>, then using <code>Axios</code>.</p>
<h3 id="heading-how-to-fetch-data-with-fetch">How to Fetch Data with <code>fetch()</code></h3>
<p>The <code>fetch()</code> method is a native browser feature that allows you to send <code>HTTP</code> requests directly from the frontend. It’s useful for making basic API calls without any additional libraries.</p>
<p>To use fetch() in React, you'll typically follow this pattern:</p>
<ul>
<li><p>Use the useEffect() hook to ensure the fetch call only runs once when the component mounts.</p>
</li>
<li><p>Call fetch('url') to send the HTTP GET request.</p>
</li>
<li><p>Use .json() to parse the JSON response.</p>
</li>
<li><p>Store the response in state using useState().</p>
</li>
</ul>
<p><strong>Let’s see an example:</strong></p>
<p>Import the necessary hooks and make the fetch call inside useEffect.js:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [users, setUsers] = useState([]);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    fetch(<span class="hljs-string">'https://jsonplaceholder.typicode.com/users'</span>)
      .then(<span class="hljs-function"><span class="hljs-params">res</span> =&gt;</span> res.json())
      .then(<span class="hljs-function"><span class="hljs-params">data</span> =&gt;</span> setUsers(data));
   <span class="hljs-comment">// res.json() converts the raw response into JSON.</span>
   <span class="hljs-comment">// setUsers(data) updates the React state with that JSON and stores it in state so you can access it.</span>

  }, []);

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"p-6 max-w-4xl mx-auto"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-2xl font-bold mb-4"</span>&gt;</span>User List (using fetch)<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">ul</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"</span>&gt;</span>
        {users.map(user =&gt; (
          <span class="hljs-tag">&lt;<span class="hljs-name">li</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{user.id}</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"bg-white shadow p-4 rounded-xl"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-lg font-semibold"</span>&gt;</span>{user.name}<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-sm text-gray-600"</span>&gt;</span>{user.email}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-sm text-gray-600"</span>&gt;</span>{user.company.name}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
        ))}
      <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
<span class="hljs-comment">//Map through the stored data in the state and display the contents</span>
<span class="hljs-comment">// in a list and access the properties from the data(user.name)</span>
  );
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App;
</code></pre>
<h3 id="heading-refactor-with-axios">Refactor with Axios</h3>
<p>Axios is a third-party library that makes HTTP requests easier and more reliable. While fetch() is built into the browser, Axios simplifies several things, like automatic JSON parsing and cleaner error handling.</p>
<h4 id="heading-why-use-axios-over-fetch">Why Use Axios Over fetch:</h4>
<ul>
<li><p>Axios automatically converts the response to JSON – you don’t need to call .json() manually.</p>
</li>
<li><p>It has built-in support for request and response interceptors.</p>
</li>
<li><p>It makes it easier to send headers, handle errors, and work with non-GET requests (POST, DELETE, and so on).</p>
</li>
</ul>
<p><strong>First, install Axios in your project via the terminal:</strong></p>
<p><code>npm install axios</code></p>
<p>Import the necessary hooks and Axios and make the API call inside useEffect.</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">import</span> { useEffect, useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> axios <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">App</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [users, setUsers] = useState([]);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    axios.get(<span class="hljs-string">'https://jsonplaceholder.typicode.com/users'</span>)
      .then(<span class="hljs-function"><span class="hljs-params">response</span> =&gt;</span> setUsers(response.data);
      <span class="hljs-comment">// response.data contains the parsed JSON from the API.</span>
      <span class="hljs-comment">// setUsers(data) updates the React state with that JSON and stores it in state so you can access it.</span>
  }, []);

  <span class="hljs-keyword">return</span> (
    <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"p-6 max-w-4xl mx-auto"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-2xl font-bold mb-4"</span>&gt;</span>User List (using Axios)
      <span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">ul</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4"</span>&gt;</span>
        {users.map(user =&gt; (
          <span class="hljs-tag">&lt;<span class="hljs-name">li</span> <span class="hljs-attr">key</span>=<span class="hljs-string">{user.id}</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"bg-white shadow p-4 rounded-xl"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-lg font-semibold"</span>&gt;</span>{user.name}<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-sm text-gray-600"</span>&gt;</span>{user.email}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"text-sm text-gray-600"</span>&gt;</span>{user.company.name}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
        ))}
      <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
<span class="hljs-comment">//Map through the stored data in the state and display the contents</span>
<span class="hljs-comment">// in a list and access the properties from the data(user.name)</span>
  );
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> App;
</code></pre>
<h2 id="heading-how-to-handle-loading-and-error-states">How to Handle Loading and Error States</h2>
<p>When working with data fetching in React, things don't always go perfectly. Sometimes data takes time to arrive and other times the request fails. Loading and Error states come in handy because they give you feedback and make it both user and developer friendly.</p>
<h3 id="heading-what-is-a-loading-state">What is a Loading State?</h3>
<p>A loading state is used to show that data is being fetched. Without it, users might not know what is happening and think the request did not go through or the app isn't working. You typically use a boolean to track this.</p>
<h3 id="heading-what-is-an-error-state">What is an Error State?</h3>
<p>An error state tells you something went wrong – maybe the API is down, or the URL was incorrect. Catching and displaying these errors helps you debug faster and gives users clear feedback.</p>
<h4 id="heading-code-snippet">Code Snippet:</h4>
<p>Here's how you might add loading and error handling to a basic fetch() request:</p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> [users, setUsers] = useState([]);
<span class="hljs-keyword">const</span> [loading, setLoading] = useState(<span class="hljs-literal">true</span>);
<span class="hljs-keyword">const</span> [error, setError] = useState(<span class="hljs-literal">null</span>);

useEffect(<span class="hljs-function">() =&gt;</span> {
  fetch(<span class="hljs-string">'https://jsonplaceholder.typicode.com/users'</span>)
    .then(<span class="hljs-function"><span class="hljs-params">res</span> =&gt;</span> {
      <span class="hljs-keyword">if</span> (!res.ok) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Network response was not ok'</span>);
      <span class="hljs-keyword">return</span> res.json();
    })
    .then(<span class="hljs-function"><span class="hljs-params">data</span> =&gt;</span> {
      setUsers(data);
      setLoading(<span class="hljs-literal">false</span>);
    })
    .catch(<span class="hljs-function"><span class="hljs-params">err</span> =&gt;</span> {
      setError(err.message);
      setLoading(<span class="hljs-literal">false</span>);
    });
}, []);

<span class="hljs-keyword">if</span> (loading) <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Loading...<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span></span>;
<span class="hljs-keyword">if</span> (error) <span class="hljs-keyword">return</span> <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Error: {error}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span></span>;
</code></pre>
<p>This gives you a smoother user experience and makes your app more reliable.</p>
<h2 id="heading-how-to-keep-your-api-keys-safe">How to Keep Your API Keys Safe</h2>
<p>If you're using an API that requires a key, it's critical to keep that key secure. Never hardcode your API keys directly into your React components or push them to public repositories. Instead, store them in a <code>.env</code> file at the root of your project(the same directory as your package.json file). In your <code>.env</code> file do this:</p>
<p><code>VITE_API_KEY=your_actual_key_here</code></p>
<p>To access it in your app, use:</p>
<p><code>const apiKey = import.meta.env.VITE_API_KEY;</code></p>
<p>You can then use this key in your API requests. Here's how you would include it in the Axios example:</p>
<pre><code class="lang-javascript">axios.get(<span class="hljs-string">`https://api.example.com/data?apikey=<span class="hljs-subst">${apiKey}</span>`</span>)
  .then(<span class="hljs-function"><span class="hljs-params">response</span> =&gt;</span> {
    setUsers(response.data);
    setLoading(<span class="hljs-literal">false</span>);
  })
  .catch(<span class="hljs-function"><span class="hljs-params">error</span> =&gt;</span> {
    setError(error.message);
    setLoading(<span class="hljs-literal">false</span>);
  });
</code></pre>
<p><strong>Note:</strong> In Vite, all environment variables must start with VITE_ to be accessible in the browser. Make sure to add .env to your .gitignore file so it doesn’t get pushed to GitHub.</p>
<p>Hiding your key helps prevent exposing your it to the public, especially if your project is shared on GitHub or deployed online.</p>
<h3 id="heading-why-this-is-important">Why this is important:</h3>
<ul>
<li><p>Exposed keys can be abused, leading to overages or bans from the API provider</p>
</li>
<li><p>You could lose access or rack up charges depending on the service</p>
</li>
<li><p>In secure apps, exposed keys can be a major security vulnerability</p>
</li>
</ul>
<p>Always treat your keys like passwords. If a key does get exposed, revoke it and generate a new one from your API provider’s dashboard.</p>
<h2 id="heading-fun-public-apis-to-practice-with">Fun Public APIs to Practice With</h2>
<p>Here are some fun and free APIs you can use to build practice projects.:</p>
<p><strong>1. JSONPlaceholder:</strong> Fake data for testing: users, posts, comments, todos. No key required.</p>
<p><strong>2. The Dog API:</strong> Get random pictures, breed info, and search by breed. Requires a free API key.</p>
<p><strong>3. The Cat API:</strong> Just like the Dog API, but for cats. Great for image-heavy apps. Free API key.</p>
<p><strong>4. PokeAPI:</strong> Fetch detailed Pokémon data. Great for cards, search filters, or games. No key required.</p>
<p><strong>5. TMDB API:</strong> Get movie data, trending shows, cast details, posters, and more. Requires a free API key from TMDB( You can clone popular streaming sites with this).</p>
<p><strong>6. REST Countries API:</strong> Retrieve country names, capitals, regions, flags, and populations. No API key required.</p>
<p><strong>7. Bored API:</strong> Get random activity suggestions for when you're bored. No key required.</p>
<p><strong>8. JokeAPI:</strong> Fetch jokes by category or type (safe for work, programming, dark humor). No key needed.</p>
<p><strong>9. Rick and Morty API:</strong> Explore characters, locations, and episodes. Perfect for fans. No key required.</p>
<p><strong>10. NASA APIs:</strong> Explore images, astronomy data, and space facts. Requires free API key from NASA.</p>
<p>Play around with different data formats, add filters or search, and combine multiple APIs into one project. It's great practice for real-world app development.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>What you've just built is the foundation of countless real-world applications. The ability to fetch, manage, and display data from APIs is essential in web development.</p>
<p>From here, you can extend this app to:</p>
<ul>
<li><p>Add search or filter functionality</p>
</li>
<li><p>Paginate the results</p>
</li>
<li><p>Display details on a separate page</p>
</li>
</ul>
<p>As you continue to grow as a developer, the patterns you practiced here will show up again and again. Mastering them now sets you up for success later.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Secure Mobile APIs in Flutter ]]>
                </title>
                <description>
                    <![CDATA[ As mobile applications continue to evolve in functionality and scope, securing the APIs that power these apps has become more critical than ever. In the context of Flutter, a framework that enables cross-platform development, understanding how to sec... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-secure-mobile-apis-in-flutter/</link>
                <guid isPermaLink="false">681a707658599a397b11b418</guid>
                
                    <category>
                        <![CDATA[ Flutter ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Dart ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Security ]]>
                    </category>
                
                    <category>
                        <![CDATA[ APIs ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Atuoha Anthony ]]>
                </dc:creator>
                <pubDate>Tue, 06 May 2025 20:26:30 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746626043471/57ea35c5-44b8-4eee-b713-ca9e735d7fe7.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>As mobile applications continue to evolve in functionality and scope, securing the APIs that power these apps has become more critical than ever.</p>
<p>In the context of Flutter, a framework that enables cross-platform development, understanding how to secure mobile APIs is essential – not only for maintaining user trust but also for safeguarding sensitive business data.</p>
<p>In this article, we’ll explore common API vulnerabilities in mobile applications, particularly Flutter apps, and outline practical strategies to mitigate these risks.</p>
<h3 id="heading-table-of-contents">Table of Contents</h3>
<ul>
<li><p><a class="post-section-overview" href="#heading-why-api-security-matters-in-mobile-apps">Why API Security Matters in Mobile Apps</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-setup-example">Project Setup Example:</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-common-vulnerabilities-in-flutter-apps">Common Vulnerabilities in Flutter Apps</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-example-secure-api-call-in-flutter">Example: Secure API Call in Flutter</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-best-practices-for-securing-apis-in-flutter-apps">Best Practices for Securing APIs in Flutter Apps</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-security-checklist-for-flutter-developers">Security Checklist for Flutter Developers</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-additional-considerations">Additional Considerations</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-references">References</a></p>
</li>
</ul>
<p>Securing API keys in a Flutter application is essential to prevent unauthorized access to sensitive resources. API keys are often used for authentication with external services – but if they’re exposed, they can lead to security vulnerabilities.</p>
<p>In this guide, we will discuss how to securely store and manage API keys using Firebase Remote Config, Flutter Secure Storage, AES encryption, and device-specific identifiers.</p>
<p>There are several ways to manage API keys securely, including:</p>
<ul>
<li><p><strong>CI/CD Solutions</strong>: Services like Codemagic, CircleCI, and GitHub Actions allow you to store API keys as environment variables to keep them out of your codebase.</p>
</li>
<li><p><strong>Backend Storage</strong>: Keeping API keys on a backend server and fetching them dynamically is another secure approach.</p>
</li>
<li><p><strong>Keystore &amp; Keychain</strong>: On Android and iOS, API keys can be securely stored using the device's built-in keystore mechanisms.</p>
</li>
<li><p><strong>Encrypted Storage</strong>: Using encrypted local storage solutions to save API keys on the device.</p>
</li>
</ul>
<h2 id="heading-why-api-security-matters-in-mobile-apps">Why API Security Matters in Mobile Apps</h2>
<p>APIs serve as the bridge between mobile applications and backend services. While they enable dynamic experiences, such as fetching user data, processing payments, and managing real-time content, they also become a major attack vector if left unsecured.</p>
<p>Mobile applications, unlike web apps, are distributed in compiled form (for example, APKs). These can be decompiled to reveal logic, endpoints, and sometimes even secrets like API keys.</p>
<p>Attackers may reverse engineer APKs, intercept traffic using proxy tools like Burp Suite, or abuse API endpoints via emulators or scripts. This can lead to data breaches, unauthorized data manipulation, or service disruption.</p>
<p>Publicly exposing API keys in your Flutter application can lead to unauthorized access and potential abuse. This can result in quota exhaustion, service disruptions, or even security breaches. Using Firebase Remote Config, encryption, and secure local storage, we can keep API keys safe.</p>
<h3 id="heading-project-setup-example"><strong>Project Setup Example:</strong></h3>
<p>For this example, we will focus on using <strong>Firebase Remote Config</strong> to securely retrieve API keys, encrypt them before storing them locally, and decrypt them when needed.</p>
<p>We will structure an implementation using the following:</p>
<ul>
<li><p><code>remote_config.dart</code>: Handles fetching and encrypting API keys.</p>
</li>
<li><p><code>global_config.dart</code>: Initializes Firebase, loads environment variables, and ensures API keys are available.</p>
</li>
<li><p><code>main.dart</code>: Starts the application and initializes configurations.</p>
</li>
<li><p><code>app_strings.dart</code>: Stores constant values used throughout the project.</p>
</li>
</ul>
<h4 id="heading-step-1-setting-up-environment-variables"><strong>Step 1: Setting Up Environment Variables</strong></h4>
<p>Create a <code>.env</code> file in your Flutter project root directory and define your encryption key:</p>
<pre><code class="lang-dart">ENCRYPTION_KEY=<span class="hljs-number">32</span>-character-secure-key-here
</code></pre>
<p>Add <code>flutter_dotenv</code> to your <code>pubspec.yaml</code>:</p>
<pre><code class="lang-dart">dependencies:
  flutter:
    sdk: flutter
  encrypt: ^<span class="hljs-number">5.0</span><span class="hljs-number">.3</span>
  flutter_dotenv: ^<span class="hljs-number">5.2</span><span class="hljs-number">.1</span>
  device_info_plus: ^<span class="hljs-number">11.3</span><span class="hljs-number">.0</span>
  firebase_remote_config: ^<span class="hljs-number">5.4</span><span class="hljs-number">.0</span>
  flutter_secure_storage: ^<span class="hljs-number">9.0</span><span class="hljs-number">.0</span>
</code></pre>
<p>Run:</p>
<pre><code class="lang-dart">flutter pub <span class="hljs-keyword">get</span>
</code></pre>
<h4 id="heading-step-2-secure-storage-and-encryption"><strong>Step 2: Secure Storage and Encryption</strong></h4>
<h3 id="heading-appstringsdart"><code>app_strings.dart</code></h3>
<p>Define constants used throughout the project:</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">AppStrings</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">String</span> ENCRYPTION_KEY = <span class="hljs-string">"ENCRYPTION_KEY"</span>;
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">String</span> DEVICE_ID = <span class="hljs-string">"DEVICE_ID"</span>;
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">String</span> YOU_VERIFY_API_KEY = <span class="hljs-string">"YOU_VERIFY_API_KEY"</span>;
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">String</span> GEMINI_API_KEY = <span class="hljs-string">"GEMINI_API_KEY"</span>;
}
</code></pre>
<h3 id="heading-remoteconfigdart"><code>remote_config.dart</code></h3>
<p>Handles secure retrieval and storage of API keys using <strong>AES encryption</strong>. This is a big one, so I’ll break down each part after the code block:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'dart:io'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:device_info_plus/device_info_plus.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/foundation.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_secure_storage/flutter_secure_storage.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:firebase_remote_config/firebase_remote_config.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../constants/app_strings.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'../../../domain/models/custom_error/custom_error.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:encrypt/encrypt.dart'</span> <span class="hljs-keyword">as</span> encrypt;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_dotenv/flutter_dotenv.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RemoteConfig</span> </span>{
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> FlutterSecureStorage _storage = FlutterSecureStorage();
  <span class="hljs-keyword">static</span> encrypt.Encrypter? _encrypter;

  <span class="hljs-comment">// Initialize AES encryption</span>
  <span class="hljs-keyword">static</span> Future&lt;<span class="hljs-keyword">void</span>&gt; initializeEncrypter() <span class="hljs-keyword">async</span> {
    encrypt.Key key = <span class="hljs-keyword">await</span> _generateEncryptionKey();
    _encrypter = encrypt.Encrypter(encrypt.AES(key, mode: encrypt.AESMode.cbc));
  }

  <span class="hljs-keyword">static</span> encrypt.Encrypter getEncrypter() {
    <span class="hljs-keyword">if</span> (_encrypter == <span class="hljs-keyword">null</span>) {
      initializeEncrypter();
    }
    <span class="hljs-keyword">return</span> _encrypter!;
  }

  <span class="hljs-comment">// Generate a secure encryption key using env variable and device ID</span>
  <span class="hljs-keyword">static</span> Future&lt;encrypt.Key&gt; _generateEncryptionKey() <span class="hljs-keyword">async</span> {
    <span class="hljs-built_in">String</span> envKey = dotenv.env[AppStrings.ENCRYPTION_KEY] ?? <span class="hljs-string">"default_secure_key"</span>;
    <span class="hljs-built_in">String</span> deviceId = <span class="hljs-keyword">await</span> _getDeviceId();
    <span class="hljs-built_in">String</span> combinedKey = (envKey + deviceId).substring(<span class="hljs-number">0</span>, <span class="hljs-number">32</span>);
    <span class="hljs-keyword">return</span> encrypt.Key.fromUtf8(combinedKey);
  }

  <span class="hljs-comment">// Fetch device ID and store it securely</span>
  <span class="hljs-keyword">static</span> Future&lt;<span class="hljs-built_in">String</span>&gt; _getDeviceId() <span class="hljs-keyword">async</span> {
    <span class="hljs-built_in">String?</span> storedDeviceId = <span class="hljs-keyword">await</span> _storage.read(key: AppStrings.DEVICE_ID);

    <span class="hljs-keyword">if</span> (storedDeviceId != <span class="hljs-keyword">null</span>) {
      <span class="hljs-keyword">return</span> storedDeviceId;
    }

    DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
    <span class="hljs-built_in">String</span> deviceId;

    <span class="hljs-keyword">if</span> (Platform.isAndroid) {
      AndroidDeviceInfo androidInfo = <span class="hljs-keyword">await</span> deviceInfo.androidInfo;
      deviceId = androidInfo.id;
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (Platform.isIOS) {
      IosDeviceInfo iosInfo = <span class="hljs-keyword">await</span> deviceInfo.iosInfo;
      deviceId = iosInfo.identifierForVendor ?? <span class="hljs-string">"fallbackDeviceId"</span>;
    } <span class="hljs-keyword">else</span> {
      deviceId = <span class="hljs-string">"fallbackDeviceId"</span>;
    }

    <span class="hljs-keyword">await</span> _storage.write(key: AppStrings.DEVICE_ID, value: deviceId);
    <span class="hljs-keyword">return</span> deviceId;
  }

  <span class="hljs-comment">// Fetch and encrypt API keys</span>
  <span class="hljs-keyword">static</span> Future&lt;<span class="hljs-keyword">void</span>&gt; fetchApiKey({<span class="hljs-keyword">required</span> <span class="hljs-built_in">String</span> apiKeyName}) <span class="hljs-keyword">async</span> {
    <span class="hljs-built_in">String</span> key = <span class="hljs-string">''</span>;
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> remoteConfig = FirebaseRemoteConfig.instance;
      <span class="hljs-keyword">await</span> remoteConfig.setConfigSettings(
        RemoteConfigSettings(
          fetchTimeout: <span class="hljs-keyword">const</span> <span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">10</span>),
          minimumFetchInterval: <span class="hljs-keyword">const</span> <span class="hljs-built_in">Duration</span>(seconds: <span class="hljs-number">10</span>),
        ),
      );
      <span class="hljs-keyword">await</span> remoteConfig.fetchAndActivate();
      key = remoteConfig.getString(apiKeyName);
    } <span class="hljs-keyword">catch</span> (e) {
      <span class="hljs-keyword">if</span> (kDebugMode) {
        <span class="hljs-built_in">print</span>(e);
      }
      <span class="hljs-keyword">throw</span> CustomError(
        errorMsg: <span class="hljs-string">"ERROR Retrieving <span class="hljs-subst">$apiKeyName</span> (<span class="hljs-subst">${e.toString()}</span>)"</span>,
        code: <span class="hljs-string">"configuration_error"</span>,
        plugin: <span class="hljs-string">""</span>,
      );
    }

    <span class="hljs-keyword">final</span> iv = encrypt.IV.fromSecureRandom(<span class="hljs-number">16</span>);
    <span class="hljs-keyword">final</span> encryptedKey = _encrypter?.encrypt(key, iv: iv).base64;

    <span class="hljs-keyword">await</span> _storage.write(key: apiKeyName, value: encryptedKey);
    <span class="hljs-keyword">await</span> _storage.write(key: <span class="hljs-string">"<span class="hljs-subst">${apiKeyName}</span>_iv"</span>, value: iv.base64);
  }

  <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">String</span>&gt; _decryptedKeysCache = {};

  <span class="hljs-comment">// Retrieve and decrypt stored API keys</span>
  <span class="hljs-keyword">static</span> Future&lt;<span class="hljs-built_in">String?</span>&gt; getApiKey({<span class="hljs-keyword">required</span> <span class="hljs-built_in">String</span> key}) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">if</span> (_decryptedKeysCache.containsKey(key)) {
      <span class="hljs-keyword">return</span> _decryptedKeysCache[key];
    }

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">final</span> encryptedKey = <span class="hljs-keyword">await</span> _storage.read(key: key);
      <span class="hljs-keyword">final</span> ivString = <span class="hljs-keyword">await</span> _storage.read(key: <span class="hljs-string">"<span class="hljs-subst">${key}</span>_iv"</span>);

      <span class="hljs-keyword">if</span> (encryptedKey != <span class="hljs-keyword">null</span> &amp;&amp; ivString != <span class="hljs-keyword">null</span>) {
        <span class="hljs-keyword">final</span> iv = encrypt.IV.fromBase64(ivString);
        <span class="hljs-keyword">final</span> encrypted = encrypt.Encrypted.fromBase64(encryptedKey);
        <span class="hljs-keyword">final</span> decryptedKey = _encrypter?.decrypt(encrypted, iv: iv);

        _decryptedKeysCache[key] = decryptedKey!;
        <span class="hljs-keyword">return</span> decryptedKey;
      }
    } <span class="hljs-keyword">catch</span> (e) {
      <span class="hljs-keyword">throw</span> CustomError(
        errorMsg: <span class="hljs-string">"ERROR Retrieving <span class="hljs-subst">$key</span> (<span class="hljs-subst">${e.toString()}</span>)"</span>,
        code: <span class="hljs-string">"configuration_error"</span>,
        plugin: <span class="hljs-string">""</span>,
      );
    }

    <span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
  }
}
</code></pre>
<p>This <code>RemoteConfig</code> class securely fetches, encrypts, stores, and retrieves sensitive API keys using Firebase Remote Config, AES encryption, secure storage, and device-specific information.</p>
<p>Here's a breakdown of what’s going on:</p>
<p><strong>1. Dependencies and Imports</strong></p>
<ul>
<li><p><code>dart:io</code>: For platform-specific checks (Android, iOS).</p>
</li>
<li><p><code>device_info_plus</code>: To get a unique device identifier.</p>
</li>
<li><p><code>flutter_secure_storage</code>: For secure local key-value storage.</p>
</li>
<li><p><code>firebase_remote_config</code>: To fetch API keys or configurations from Firebase.</p>
</li>
<li><p><code>encrypt</code>: For AES encryption and decryption.</p>
</li>
<li><p><code>flutter_dotenv</code>: To read environment variables.</p>
</li>
<li><p><code>CustomError</code>: A custom error model used for error handling.</p>
</li>
<li><p><code>AppStrings</code>: Presumably holds constant strings like keys.</p>
</li>
</ul>
<p><strong>2.</strong> <strong>Class Properties</strong></p>
<pre><code class="lang-dart"><span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> FlutterSecureStorage _storage = FlutterSecureStorage();
<span class="hljs-keyword">static</span> encrypt.Encrypter? _encrypter;
</code></pre>
<ul>
<li><p><code>_storage</code>: For securely storing encrypted keys and IVs.</p>
</li>
<li><p><code>_encrypter</code>: Used to encrypt and decrypt data using AES.</p>
</li>
</ul>
<p><strong>3.</strong> <code>initializeEncrypter()</code></p>
<pre><code class="lang-dart"><span class="hljs-keyword">static</span> Future&lt;<span class="hljs-keyword">void</span>&gt; initializeEncrypter() <span class="hljs-keyword">async</span>
</code></pre>
<ul>
<li><p>Sets up the AES encryptor using a combination of a <code>.env</code> key and the device ID to generate a 32-byte key.</p>
</li>
<li><p>Uses AES CBC mode.</p>
</li>
</ul>
<p><strong>4.</strong> <code>getEncrypter()</code></p>
<pre><code class="lang-dart"><span class="hljs-keyword">static</span> encrypt.Encrypter getEncrypter()
</code></pre>
<ul>
<li>Returns the existing encryptor or calls <code>initializeEncrypter()</code> if it's not yet initialized.</li>
</ul>
<p><strong>5.</strong> <code>_generateEncryptionKey()</code></p>
<pre><code class="lang-dart"><span class="hljs-keyword">static</span> Future&lt;encrypt.Key&gt; _generateEncryptionKey()
</code></pre>
<ul>
<li><p>Combines an environment variable (<code>ENCRYPTION_KEY</code>) and the device ID to produce a 32-character key.</p>
</li>
<li><p>Returns an AES key (<code>encrypt.Key.fromUtf8</code>).</p>
</li>
</ul>
<p><strong>6.</strong> <code>_getDeviceId()</code></p>
<pre><code class="lang-dart"><span class="hljs-keyword">static</span> Future&lt;<span class="hljs-built_in">String</span>&gt; _getDeviceId()
</code></pre>
<ul>
<li><p>Checks if a device ID is already securely stored. If not, gets it from the device (Android: <a target="_blank" href="http://androidInfo.id"><code>androidInfo.id</code></a>, iOS: <code>identifierForVendor</code>).</p>
</li>
<li><p>Stores the device ID securely for future use.</p>
</li>
</ul>
<p><strong>7.</strong> <code>fetchApiKey()</code></p>
<pre><code class="lang-dart"><span class="hljs-keyword">static</span> Future&lt;<span class="hljs-keyword">void</span>&gt; fetchApiKey({<span class="hljs-keyword">required</span> <span class="hljs-built_in">String</span> apiKeyName})
</code></pre>
<ul>
<li><p>Fetches the specified API key from Firebase Remote Config.</p>
</li>
<li><p>Encrypts the key using AES and a random IV (initialization vector).</p>
</li>
<li><p>Stores both the encrypted key and the IV securely.</p>
</li>
</ul>
<p><strong>8.</strong> <code>getApiKey()</code></p>
<pre><code class="lang-dart"><span class="hljs-keyword">static</span> Future&lt;<span class="hljs-built_in">String?</span>&gt; getApiKey({<span class="hljs-keyword">required</span> <span class="hljs-built_in">String</span> key})
</code></pre>
<ul>
<li><p>Retrieves and decrypts the encrypted API key.</p>
</li>
<li><p>If already decrypted and cached in memory, returns it immediately.</p>
</li>
<li><p>Otherwise:</p>
<ul>
<li><p>Reads the encrypted key and IV from secure storage.</p>
</li>
<li><p>Decrypts the key and returns it.</p>
</li>
<li><p>Caches the decrypted result in <code>_decryptedKeysCache</code>.</p>
</li>
</ul>
</li>
</ul>
<p><strong>9. Error Handling</strong></p>
<p>Custom <code>CustomError</code> exceptions are thrown if Firebase fetch or decryption fails.</p>
<p>This class is built to:</p>
<ul>
<li><p>Securely fetch API keys from Firebase.</p>
</li>
<li><p>Encrypt them using a key tied to both an environment variable and the specific device.</p>
</li>
<li><p>Store them locally in an encrypted form.</p>
</li>
<li><p>Allow retrieval and decryption with in-memory caching to minimize processing overhead.</p>
</li>
</ul>
<h4 id="heading-step-3-global-initialization"><strong>Step 3: Global Initialization</strong></h4>
<h3 id="heading-globalconfigdart"><code>global_config.dart</code></h3>
<p>Handles Firebase initialization, dependency injection, and API key retrieval:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:firebase_core/firebase_core.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/widgets.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_dotenv/flutter_dotenv.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:injectable/injectable.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'remote_config.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'app_strings.dart'</span>;

<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">GlobalConfig</span> </span>{
  <span class="hljs-keyword">static</span> Future&lt;<span class="hljs-keyword">void</span>&gt; fetchRequiredApiKeys() <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> apiKeys = [
      AppStrings.YOU_VERIFY_API_KEY,
      AppStrings.GEMINI_API_KEY,
    ];
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">final</span> keyName <span class="hljs-keyword">in</span> apiKeys) {
      <span class="hljs-keyword">await</span> RemoteConfig.fetchApiKey(apiKeyName: keyName);
    }
  }

  <span class="hljs-keyword">static</span> Future&lt;<span class="hljs-keyword">void</span>&gt; initConfig() <span class="hljs-keyword">async</span> {
    WidgetsFlutterBinding.ensureInitialized();
    <span class="hljs-keyword">await</span> Firebase.initializeApp();
    <span class="hljs-keyword">await</span> dotenv.load(fileName: <span class="hljs-string">".env"</span>);
    <span class="hljs-keyword">await</span> RemoteConfig.initializeEncrypter();
    <span class="hljs-keyword">await</span> fetchRequiredApiKeys();
  }
}
</code></pre>
<h4 id="heading-step-4-utilizing-the-api-key-in-ui"><strong>Step 4: Utilizing the API Key in UI</strong></h4>
<h3 id="heading-maindart"><code>main.dart</code></h3>
<p>Initializes the application:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter/material.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'global_config.dart'</span>;

Future&lt;<span class="hljs-keyword">void</span>&gt; main() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">await</span> GlobalConfig.initConfig();
  runApp(MyApp());
}
</code></pre>
<h4 id="heading-step-5-fetching-api-key-in-widget"><strong>Step 5: Fetching API Key in Widget</strong></h4>
<pre><code class="lang-dart"><span class="hljs-built_in">String</span> apiKey = <span class="hljs-string">""</span>;

<span class="hljs-meta">@override</span>
<span class="hljs-keyword">void</span> initState() {
  <span class="hljs-keyword">super</span>.initState();
  fetchAPIKey();
}

<span class="hljs-keyword">void</span> fetchAPIKey() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">final</span> key = <span class="hljs-keyword">await</span> RemoteConfig.getApiKey(key: AppStrings.GEMINI_API_KEY) ?? <span class="hljs-string">""</span>;
    setState(() {
      apiKey = key;
    });
  } <span class="hljs-keyword">catch</span> (e) {
    <span class="hljs-built_in">print</span>(<span class="hljs-string">"Error fetching API key: <span class="hljs-subst">$e</span>"</span>);
  }
}
</code></pre>
<h2 id="heading-common-vulnerabilities-in-flutter-apps">Common Vulnerabilities in Flutter Apps</h2>
<h3 id="heading-1-hardcoding-secrets">1. Hardcoding Secrets</h3>
<p>Storing API keys or secrets in the codebase (even in <code>.env</code> or <code>.dart</code> files) is one of the most dangerous mistakes. Tools like <code>apktool</code> can extract these secrets easily from the compiled binary.</p>
<p><strong>Avoid this:</strong></p>
<pre><code class="lang-dart"><span class="hljs-comment">// Do not hardcode keys</span>
<span class="hljs-keyword">const</span> apiKey = <span class="hljs-string">'YOUR_SECRET_API_KEY'</span>;
</code></pre>
<p>Hardcoding secrets is unsafe because when the APK is reverse-engineered, anyone can read those values and misuse your APIs.</p>
<p><strong>Use secure storage instead:</strong></p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_secure_storage/flutter_secure_storage.dart'</span>;

<span class="hljs-keyword">final</span> storage = FlutterSecureStorage();
<span class="hljs-keyword">await</span> storage.write(key: <span class="hljs-string">'api_key'</span>, value: <span class="hljs-string">'your_api_key'</span>);
<span class="hljs-keyword">final</span> apiKey = <span class="hljs-keyword">await</span> storage.read(key: <span class="hljs-string">'api_key'</span>);
</code></pre>
<p>Using <code>flutter_secure_storage</code> stores secrets securely in platform-specific secure storage mechanisms like Android's Keystore or iOS's Keychain.</p>
<h3 id="heading-2-lack-of-ssltls-enforcement-mitm-attacks">2. Lack of SSL/TLS Enforcement (MITM Attacks)</h3>
<p>A Man-in-the-Middle (MITM) attack occurs when an attacker intercepts and potentially alters communication between two parties. This is especially dangerous in unsecured HTTP connections, as sensitive information like login credentials and API keys can be stolen or modified.</p>
<h4 id="heading-how-ssltls-secures-code">How SSL/TLS Secures Code:</h4>
<p>Secure Sockets Layer (SSL) and Transport Layer Security (TLS) are cryptographic protocols that ensure encrypted communication between a client and a server. This prevents MITM attacks by ensuring that the data is encrypted and cannot be read or altered while in transit. The connection is established over HTTPS (which is HTTP over SSL/TLS).</p>
<p><strong>Code Example to Enforce SSL/TLS:</strong></p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:http/http.dart'</span> <span class="hljs-keyword">as</span> http;

<span class="hljs-keyword">void</span> makeSecureRequest() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> http.<span class="hljs-keyword">get</span>(<span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'https://yourapi.com/endpoint'</span>));

  <span class="hljs-keyword">if</span> (response.statusCode == <span class="hljs-number">200</span>) {
    <span class="hljs-comment">// Handle successful response</span>
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-comment">// Handle error</span>
  }
}
</code></pre>
<p>In this case, making sure that the URL starts with <code>https://</code> enforces the use of SSL/TLS for secure communication</p>
<h3 id="heading-3-weak-authentication">3. Weak Authentication</h3>
<p>Weak authentication methods are those that are easily guessed or bypassed, such as simple passwords, lack of multi-factor authentication, or weak hashing mechanisms.</p>
<p>Instead, you should use robust authentication methods like Firebase and OAuth.</p>
<ul>
<li><p><strong>Firebase Authentication</strong> provides various authentication methods such as email/password login, Google sign-in, and phone number authentication. It is a secure and easy-to-implement solution.</p>
</li>
<li><p><strong>OAuth</strong> is a protocol that allows third-party services (like Google or Facebook) to securely authenticate users without exposing their password to your app. OAuth uses tokens for authorization, ensuring that user credentials are not compromised.</p>
</li>
</ul>
<p><strong>Use Firebase Auth or OAuth2:</strong></p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:firebase_auth/firebase_auth.dart'</span>;

<span class="hljs-keyword">final</span> FirebaseAuth _auth = FirebaseAuth.instance;
UserCredential userCredential = <span class="hljs-keyword">await</span> _auth.signInWithEmailAndPassword(
  email: <span class="hljs-string">'user@example.com'</span>,
  password: <span class="hljs-string">'securePassword'</span>,
);
<span class="hljs-keyword">final</span> token = <span class="hljs-keyword">await</span> userCredential.user?.getIdToken();
</code></pre>
<p>Token-based authentication allows the backend to verify the identity of the user securely without relying on session cookies. Firebase Authentication handles token generation, validation, and expiration for you.</p>
<h3 id="heading-4-insufficient-authorization-checks">4. Insufficient Authorization Checks</h3>
<p>Authorization checks are necessary to ensure that the authenticated user has the required permissions to perform certain actions. For example, an admin user may have access to all endpoints, while a regular user may only have access to limited resources.</p>
<h4 id="heading-how-to-verify-user-rolespermissions">How to Verify User Roles/Permissions:</h4>
<p>On the server side, roles and permissions are typically stored in a database. When a user makes a request, the server checks their role and compares it against the required permissions for the requested resource.</p>
<p><strong>Code Example</strong>:</p>
<pre><code class="lang-dart"><span class="hljs-comment">// Assuming user roles are stored in Firestore</span>
Future&lt;<span class="hljs-built_in">bool</span>&gt; checkUserRole(<span class="hljs-built_in">String</span> userId, <span class="hljs-built_in">String</span> requiredRole) <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> userDoc = <span class="hljs-keyword">await</span> FirebaseFirestore.instance.collection(<span class="hljs-string">'users'</span>).doc(userId).<span class="hljs-keyword">get</span>();
  <span class="hljs-keyword">final</span> userRole = userDoc.data()?[<span class="hljs-string">'role'</span>];

  <span class="hljs-keyword">if</span> (userRole == requiredRole) {
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">true</span>;
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">throw</span> CustomError(errorMsg: <span class="hljs-string">'User does not have the required role'</span>);
  }
}
</code></pre>
<p>Authorization ensures the user is not only authenticated but also has the rights to perform specific actions.</p>
<h3 id="heading-5-exposed-endpoints-and-metadata">5. Exposed Endpoints and Metadata</h3>
<p>Exposing Swagger documentation or test endpoints in production can allow attackers to easily discover vulnerabilities in your API. It provides them with detailed information about the structure and capabilities of your API, which can be exploited.</p>
<h4 id="heading-how-to-secure-with-route-guards">How to Secure with Route Guards:</h4>
<p>A route guard can prevent unauthorized access to sensitive routes, ensuring that only authenticated and authorized users can access certain endpoints.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">void</span> checkRouteAccess(<span class="hljs-built_in">String</span> route) {
  <span class="hljs-keyword">if</span> (!isUserAuthenticated()) {
    <span class="hljs-keyword">throw</span> CustomError(errorMsg: <span class="hljs-string">'User not authorized'</span>);
  }
}
</code></pre>
<p><strong>Avoid this:</strong></p>
<ul>
<li><p>Don't deploy Swagger docs without authentication</p>
</li>
<li><p>Use route guards for admin/dev routes</p>
</li>
<li><p>Strip debug symbols and logs in production builds</p>
</li>
</ul>
<p><strong>Example: Secure API Call in Flutter</strong></p>
<p>Here’s a simple example using Dio, a powerful HTTP client for Dart, to securely call an API with token-based authentication and HTTPS:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">import</span> <span class="hljs-string">'package:dio/dio.dart'</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">'package:flutter_secure_storage/flutter_secure_storage.dart'</span>;

<span class="hljs-keyword">final</span> dio = Dio();
<span class="hljs-keyword">final</span> storage = FlutterSecureStorage();

Future&lt;<span class="hljs-keyword">void</span>&gt; fetchSecureData() <span class="hljs-keyword">async</span> {
  <span class="hljs-keyword">final</span> token = <span class="hljs-keyword">await</span> storage.read(key: <span class="hljs-string">'auth_token'</span>);

  dio.options.headers[<span class="hljs-string">'Authorization'</span>] = <span class="hljs-string">'Bearer <span class="hljs-subst">$token</span>'</span>;

  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> dio.<span class="hljs-keyword">get</span>(<span class="hljs-string">'https://yourapi.com/secure-endpoint'</span>);
    <span class="hljs-built_in">print</span>(response.data);
  } <span class="hljs-keyword">catch</span> (e) {
    <span class="hljs-built_in">print</span>(<span class="hljs-string">'API call failed: <span class="hljs-subst">$e</span>'</span>);
  }
}
</code></pre>
<p>This example illustrates how to include an authorization token in your request header and securely make an HTTPS request using <code>dio</code>. Dio also supports interceptors, retries, and advanced options like certificate pinning.</p>
<h2 id="heading-best-practices-for-securing-apis-in-flutter-apps">Best Practices for Securing APIs in Flutter Apps</h2>
<h3 id="heading-always-use-https"><strong>Always Use HTTPS</strong></h3>
<p>Avoid plain HTTP at all costs. Use HTTPS to encrypt data in transit.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">final</span> response = <span class="hljs-keyword">await</span> http.<span class="hljs-keyword">get</span>(<span class="hljs-built_in">Uri</span>.parse(<span class="hljs-string">'https://api.secure.com/data'</span>));
</code></pre>
<h3 id="heading-implement-oauth2-or-firebase-auth"><strong>Implement OAuth2 or Firebase Auth</strong></h3>
<p>Use modern authentication packages like <code>firebase_auth</code> or <code>oauth2_client</code>. These offer secure, token-based authentication with built-in session and refresh token management.</p>
<h3 id="heading-use-firebase-app-check"><strong>Use Firebase App Check</strong></h3>
<p>Prevents abuse of your backend by verifying the legitimacy of the client app.</p>
<pre><code class="lang-dart"><span class="hljs-keyword">await</span> FirebaseAppCheck.instance.activate(
  webRecaptchaSiteKey: <span class="hljs-string">'your-site-key'</span>,
);
</code></pre>
<h3 id="heading-secure-storage-of-sensitive-data"><strong>Secure Storage of Sensitive Data</strong></h3>
<p>Use <code>flutter_secure_storage</code> to safely store tokens and secrets locally.</p>
<h3 id="heading-obfuscate-dart-code"><strong>Obfuscate Dart Code</strong></h3>
<p>Obfuscation makes your Dart code harder to reverse-engineer. You can do this by renaming classes, methods, and variables into meaningless names.</p>
<pre><code class="lang-bash">flutter build apk --obfuscate --split-debug-info=build/symbols
</code></pre>
<p>This command strips debug information and renames classes/functions, making it harder for attackers to understand your compiled code.</p>
<h3 id="heading-use-rate-limiting-and-throttling"><strong>Use Rate Limiting and Throttling</strong></h3>
<p>Protect backend APIs from abuse by rate-limiting requests. Implement server-side rate-limiting using API Gateway tools or middleware libraries. <a target="_blank" href="https://www.freecodecamp.org/news/what-is-rate-limiting-web-apis/">Here’s a tutorial</a> that’ll teach you more about this technique.</p>
<h3 id="heading-set-up-logging-and-monitoring"><strong>Set Up Logging and Monitoring</strong></h3>
<p>Use tools like Firebase Crashlytics or Sentry to track errors and suspicious activity.</p>
<pre><code class="lang-dart">FirebaseCrashlytics.instance.recordError(e, stackTrace);
</code></pre>
<h3 id="heading-api-gateway-and-waf"><strong>API Gateway and WAF</strong></h3>
<p>Use API management layers like Google Cloud Endpoints or AWS API Gateway along with Web Application Firewalls (WAF) to control and filter traffic.</p>
<h2 id="heading-security-checklist-for-flutter-developers">Security Checklist for Flutter Developers</h2>
<ul>
<li><p>Use HTTPS for all communications</p>
</li>
<li><p>Never hardcode secrets or credentials</p>
</li>
<li><p>Use token-based authentication (OAuth2, Firebase Auth)</p>
</li>
<li><p>Validate tokens on both client and server</p>
</li>
<li><p>Obfuscate and minify code before production</p>
</li>
<li><p>Securely store sensitive data using <code>flutter_secure_storage</code></p>
</li>
<li><p>Enable Firebase App Check or equivalent</p>
</li>
<li><p>Use API Gateways and WAFs for traffic filtering</p>
</li>
<li><p>Monitor usage logs and set up alerts for anomalies</p>
</li>
<li><p>Implement rate limiting to prevent abuse</p>
</li>
</ul>
<h3 id="heading-additional-considerations">Additional Considerations</h3>
<h4 id="heading-certificate-pinning"><strong>Certificate Pinning:</strong></h4>
<p>Certificate pinning is a technique used to ensure that the app only communicates with a trusted server by comparing the server's certificate against a pre-stored certificate or public key. This prevents attackers from using fraudulent certificates.</p>
<p>Example: Certificate Pinning in Dio</p>
<pre><code class="lang-dart"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">CertPinningInterceptor</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">Interceptor</span> </span>{
  <span class="hljs-meta">@override</span>
  <span class="hljs-keyword">void</span> onRequest(RequestOptions options, RequestInterceptorHandler handler) <span class="hljs-keyword">async</span> {
    <span class="hljs-keyword">final</span> context = SecurityContext(withTrustedRoots: <span class="hljs-keyword">false</span>);
    <span class="hljs-keyword">final</span> certBytes = (<span class="hljs-keyword">await</span> rootBundle.load(<span class="hljs-string">'assets/certs/myserver.cer'</span>)).buffer.asUint8List();
    context.setTrustedCertificatesBytes(certBytes);

    <span class="hljs-keyword">final</span> client = HttpClient(context: context);
    client.badCertificateCallback = (X509Certificate cert, <span class="hljs-built_in">String</span> host, <span class="hljs-built_in">int</span> port) {
      <span class="hljs-keyword">final</span> serverSha = sha256.convert(cert.der).toString();
      <span class="hljs-keyword">const</span> expectedSha = <span class="hljs-string">'your_cert_sha256_fingerprint'</span>;
      <span class="hljs-keyword">return</span> serverSha == expectedSha;
    };

    (options.extra[<span class="hljs-string">'httpClientAdapter'</span>] <span class="hljs-keyword">as</span> DefaultHttpClientAdapter?)
        ?.onHttpClientCreate = (_) =&gt; client;

    handler.next(options);
  }
}
</code></pre>
<ul>
<li><p><code>SecurityContext(withTrustedRoots: false)</code>: Starts with an empty trust store, meaning no system CAs are trusted by default.</p>
</li>
<li><p><code>setTrustedCertificatesBytes</code>: Loads your own server’s certificate from local assets and sets it as the only trusted certificate.</p>
</li>
<li><p><code>HttpClient.badCertificateCallback</code>: Compares the server’s certificate SHA-256 fingerprint against a known good value. If they match, the request proceeds.</p>
</li>
<li><p><code>onHttpClientCreate</code>: Replaces the default Dio HTTP client with the custom client configured for pinning.</p>
</li>
</ul>
<p>This ensures that your app will only accept HTTPS connections from your own trusted server, protecting users from certificate spoofing or MITM attacks.</p>
<h4 id="heading-ttl-and-token-rotation"><strong>TTL and Token Rotation:</strong></h4>
<p><strong>Time-to-Live (TTL)</strong> is a security measure that ensures tokens automatically expire after a defined period. This limits the duration a token can be used, reducing the attack surface if it’s compromised.</p>
<p><strong>Token Rotation</strong> enhances security further by issuing a new refresh token every time the existing one is used to request a new access token. The previous refresh token is then invalidated. This prevents replay attacks where an attacker might attempt to reuse a stolen refresh token.</p>
<p><strong>Real-World Token Lifecycle:</strong></p>
<ol>
<li><p><strong>Access Token</strong>:</p>
<ul>
<li><p><strong>TTL</strong>: Short (for example, 15 minutes)</p>
</li>
<li><p><strong>Purpose</strong>: Used to authenticate and authorize API requests</p>
</li>
<li><p><strong>Behavior</strong>: Expires quickly to minimize risk if exposed</p>
</li>
</ul>
</li>
<li><p><strong>Refresh Token</strong>:</p>
<ul>
<li><p><strong>TTL</strong>: Longer (for example, 7 days)</p>
</li>
<li><p><strong>Purpose</strong>: Used to request new access tokens without requiring the user to log in again</p>
</li>
<li><p><strong>Rotation</strong>: A new refresh token is issued with each use</p>
</li>
</ul>
</li>
</ol>
<p>Here’s an example Implementation (Dart-like Pseudo-code):</p>
<p>Generate an access token (15-minute TTL):</p>
<pre><code class="lang-dart"><span class="hljs-built_in">String</span> generateAccessToken(<span class="hljs-built_in">String</span> userId) {
  <span class="hljs-keyword">final</span> expiry = <span class="hljs-built_in">DateTime</span>.now().add(<span class="hljs-built_in">Duration</span>(minutes: <span class="hljs-number">15</span>));
  <span class="hljs-keyword">return</span> createJwtToken(userId: userId, expiresAt: expiry);
}
</code></pre>
<p>Then generate a refresh token (7-day TTL):</p>
<pre><code class="lang-dart"><span class="hljs-built_in">String</span> generateRefreshToken(<span class="hljs-built_in">String</span> userId) {
  <span class="hljs-keyword">final</span> expiry = <span class="hljs-built_in">DateTime</span>.now().add(<span class="hljs-built_in">Duration</span>(days: <span class="hljs-number">7</span>));
  <span class="hljs-keyword">return</span> createSecureRandomToken(userId: userId, expiresAt: expiry);
}
</code></pre>
<p>Refresh the endpoint with rotation:</p>
<pre><code class="lang-dart"><span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">String</span>&gt; refreshAccessToken(<span class="hljs-built_in">String</span> refreshToken) {
  <span class="hljs-keyword">if</span> (isValidRefreshToken(refreshToken)) {
    <span class="hljs-keyword">final</span> userId = getUserIdFromRefreshToken(refreshToken);

    <span class="hljs-comment">// Invalidate old refresh token</span>
    invalidateRefreshToken(refreshToken);

    <span class="hljs-comment">// Rotate tokens</span>
    <span class="hljs-keyword">final</span> newRefreshToken = generateRefreshToken(userId);
    <span class="hljs-keyword">final</span> newAccessToken = generateAccessToken(userId);

    <span class="hljs-keyword">return</span> {
      <span class="hljs-string">'accessToken'</span>: newAccessToken,
      <span class="hljs-string">'refreshToken'</span>: newRefreshToken,
    };
  } <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">throw</span> CustomError(errorMsg: <span class="hljs-string">'Invalid or expired refresh token'</span>);
  }
}
</code></pre>
<p>Why this matters:</p>
<ul>
<li><p><strong>Mitigates long-term exposure</strong>: Tokens automatically expire, reducing risk.</p>
</li>
<li><p><strong>Prevents replay attacks</strong>: A rotated refresh token cannot be reused if intercepted.</p>
</li>
<li><p><strong>Enhances session security</strong>: Even if a token is stolen, it becomes useless quickly.</p>
</li>
</ul>
<h4 id="heading-backend-validation"><strong>Backend Validation:</strong></h4>
<p>Backend validation ensures that sensitive data, like API keys or JWT tokens, is checked on the server side, preventing tampering from malicious users.</p>
<p>Never trust the client. Always re-validate all sensitive operations and user roles on the backend.</p>
<p>Example:</p>
<pre><code class="lang-dart"><span class="hljs-keyword">void</span> validateToken(<span class="hljs-built_in">String</span> token) {
  <span class="hljs-keyword">if</span> (isTokenExpired(token)) {
    <span class="hljs-keyword">throw</span> CustomError(errorMsg: <span class="hljs-string">'Token expired'</span>);
  }
}
</code></pre>
<ul>
<li><p><code>validateToken(String token)</code>: A function that takes a user's token as input.</p>
</li>
<li><p><code>isTokenExpired(token)</code>: A hypothetical function that checks whether the token has expired (e.g., by decoding the token and checking its expiry timestamp).</p>
</li>
<li><p><code>throw CustomError(...)</code>: If the token is expired, an error is thrown — in this case, a <code>CustomError</code> with a message saying <code>'Token expired'</code>.</p>
</li>
</ul>
<p>Why this matters:</p>
<ul>
<li><p>Tokens can be stolen or manipulated on the client side, so trusting them blindly is dangerous.</p>
</li>
<li><p>Backend checks like this help enforce server-side control over user authentication and session validity.</p>
</li>
<li><p>Even if a user tampers with client-side code, they can't bypass this server-side validation.</p>
</li>
</ul>
<h4 id="heading-use-security-focused-tools-like-owasp-zapburp-suitepostman"><strong>Use Security-focused Tools like OWASP ZAP/Burp Suite/Postman:</strong></h4>
<p>Use tools like OWASP ZAP, Burp Suite, and Postman to manually and automatically test your API endpoints for vulnerabilities.</p>
<ul>
<li><p><strong>OWASP ZAP</strong>: Used for penetration testing, finding vulnerabilities like XSS, SQL Injection, and so on.</p>
</li>
<li><p><strong>Burp Suite</strong>: Another tool for testing security vulnerabilities in web apps.</p>
</li>
<li><p><strong>Postman</strong>: Can be used for testing API endpoints and ensuring secure communications by adding necessary headers like <code>Authorization</code>.</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Securing mobile APIs is a foundational requirement in modern app development. For Flutter developers, this means going beyond building beautiful UIs to ensuring the underlying API infrastructure is resilient against threats. The risks of exposed endpoints, leaked secrets, and insecure communication are very real, but preventable.</p>
<p>Security is about proactive defense, and you should make it a core part of your development workflow. With consistent practices, regular audits, and attention to detail, you’ll protect both your users and your product from unnecessary risks. Flutter provides the flexibility and power to build fast, cross-platform apps – so don’t let poor API security undermine that potential.</p>
<p>By following the best practices outlined in this article, such as using HTTPS, implementing proper authentication and authorization, securely storing credentials, and leveraging tools like Firebase App Check, you can significantly reduce your app’s attack surface.</p>
<p>Remember: effective security starts with a mindset. It’s not just a one-time setup, but an ongoing process of vigilance, testing, and improvement.</p>
<h3 id="heading-references">References</h3>
<ul>
<li><p><a target="_blank" href="https://owasp.org/www-project-mobile-security/">OWASP Mobile Security Project</a></p>
</li>
<li><p><a target="_blank" href="https://owasp.org/www-project-api-security/">OWASP API Security Top 10</a></p>
</li>
<li><p><a target="_blank" href="https://developer.android.com/topic/security/best-practices">Android Security Best Practices</a></p>
</li>
<li><p><a target="_blank" href="https://pub.dev/packages/flutter_secure_storage"><strong>Flutter Secure Storage - pub.dev</strong></a></p>
</li>
<li><p><a target="_blank" href="https://pub.dev/packages/encrypt"><strong>Encrypt Package - pub.dev</strong></a></p>
</li>
<li><p><a target="_blank" href="https://firebase.google.com/docs/remote-config"><strong>Firebase Remote Config - Firebase Docs</strong></a></p>
</li>
<li><p><a target="_blank" href="https://pub.dev/packages/device_info_plus"><strong>Device Info Plus - pub.dev</strong></a></p>
</li>
<li><p><a target="_blank" href="https://pub.dev/packages/flutter_dotenv"><strong>Flutter dotenv - pub.dev</strong></a></p>
</li>
<li><p><a target="_blank" href="https://pub.dev/packages/injectable"><strong>Injectable for Dependency Injection - pub.dev</strong></a></p>
</li>
<li><p><a target="_blank" href="https://firebase.flutter.dev/docs/overview/"><strong>Flutter Fire (Firebase Initialization) - Firebase Docs</strong></a></p>
</li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
