<?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[ David Aniebo - 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[ David Aniebo - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Mon, 01 Jun 2026 18:36:26 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/davidaniebo/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Design APIs for AI Agents ]]>
                </title>
                <description>
                    <![CDATA[ APIs are designed for human developers. People read documentation, infer the intent behind an endpoint, and know how to handle edge cases when something unexpected happens. AI agents don't have that c ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-design-apis-for-ai-agents/</link>
                <guid isPermaLink="false">6a18bdb078258754833f8205</guid>
                
                    <category>
                        <![CDATA[ llm ]]>
                    </category>
                
                    <category>
                        <![CDATA[ api ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ai-agent ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ David Aniebo ]]>
                </dc:creator>
                <pubDate>Thu, 28 May 2026 22:12:00 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/056b20d6-7409-4b6e-a29c-0b48061a7508.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>APIs are designed for human developers. People read documentation, infer the intent behind an endpoint, and know how to handle edge cases when something unexpected happens.</p>
<p>AI agents don't have that context and understanding.</p>
<p>AI agent understand APIs through schemas, examples, randomized data and live responses. When a behavior or method is ambiguous and inconsistent, the model doesn't pause to “think” – it fills in the blanks (randomizing).</p>
<p>In production, those guesses could become blocks, retry storms, duplicated side effects, or broken workflows.</p>
<p>This is why APIs that are perfectly fine for humans frequently fail under AI agent use. The problem is rarely “the agent isn’t smart enough.” More often, the API was never designed for an agent/machine consumer that must plan, call tools, and recover from failure without a human in the loop.</p>
<p>In this guide, you’ll learn how to design APIs that agents can use reliably. We’ll anchor the discussion in three practical ideas:</p>
<ol>
<li><p><strong>Deterministic behavior:</strong> same inputs and state should yield predictable outcomes and shapes.</p>
</li>
<li><p><strong>Strong schemas:</strong> contracts that are complete, descriptive, and testable.</p>
</li>
<li><p><strong>Guardrails at the API boundary:</strong> authorization, validation, and safe defaults that prevent unsafe autonomy.</p>
</li>
</ol>
<p>The aim of this article is not to build “AI-powered” APIs, but rather to build APIs that are <strong>clear, strict,</strong> and <strong>dependable,</strong> even when the caller is not an agent but a fellow developers leveraging various tools.</p>
<h2 id="heading-table-of-contents">Table Of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-why-good-enough-for-devs-is-not-good-enough-for-agents">Why “Good Enough for Devs” Is Not Good Enough for Agents</a></p>
</li>
<li><p><a href="#heading-principle-1-deterministic-behavior">Principle 1: Deterministic Behavior</a></p>
</li>
<li><p><a href="#heading-principle-2-strong-schemas">Principle 2: Strong Schemas</a></p>
</li>
<li><p><a href="#heading-principle-3-guardrails-at-the-api-boundary">Principle 3: Guardrails at the API Boundary</a></p>
</li>
<li><p><a href="#heading-patterns-that-bridge-apis-and-agent-runtimes">Patterns That Bridge APIs and Agent Runtimes</a></p>
</li>
<li><p><a href="#heading-a-practical-before-and-after-example">A Practical Before and After Example</a></p>
</li>
<li><p><a href="#heading-checklist-is-your-api-agent-ready">Checklist: Is Your API Agent-Ready?</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before reading this guide, it helps to have:</p>
<ul>
<li><p>A basic understanding of HTTP APIs and REST concepts</p>
</li>
<li><p>Familiarity with JSON and API request/response patterns</p>
</li>
<li><p>An understanding of common API concepts like authentication, pagination, and retries</p>
</li>
</ul>
<h2 id="heading-why-good-enough-for-devs-is-not-good-enough-for-agents">Why “Good Enough for Devs” Is Not Good Enough for Agents</h2>
<p>Human developers bring implied and contextual knowledge: they read through Slack threads, read blog posts, and recognize that “this 404 usually means you forgot the workspace ID.”</p>
<p>Agents mostly get whatever is in the spec, the examples, and the last response body.</p>
<p>That gap shows up in predictable ways:</p>
<ul>
<li><p><strong>Ambiguous semantics:</strong> wrong endpoint or wrong parameter combination.</p>
</li>
<li><p><strong>Undocumented branches:</strong> the model invents fields or misreads optional behavior.</p>
</li>
<li><p><strong>Inconsistent error bodies:</strong> retries that shouldn't happen, or no retry when one is safe.</p>
</li>
<li><p><strong>Non-idempotent “do things” endpoints:</strong> duplicate charges, duplicate tickets, duplicate emails.</p>
</li>
</ul>
<p>Industry commentary and practitioner guides converge on the same point: agents are becoming a major class of API consumer, and machine legibility matters as much as developer experience.</p>
<p>See for example discussions of OpenAPI as the source of truth for agents, emerging tool protocols, and traffic patterns that differ from human clients in the resources listed at the end of this article.</p>
<h2 id="heading-principle-1-deterministic-behavior">Principle 1: Deterministic Behavior</h2>
<p>Determinism for agents doesn't mean “always return the same JSON forever.” It means: <strong>given the same request and the same server-side state, your API behaves in a way the agent can model</strong> and when state changes, you make that explicit.</p>
<h3 id="heading-prefer-explicit-state-over-hidden-magic">Prefer Explicit State Over Hidden Magic</h3>
<p>Agents struggle with “sometimes the server does X depending on internal flags.” Where humans infer intent from product copy, agents infer from patterns. If those patterns drift, autonomy breaks.</p>
<p>Practical habits:</p>
<ul>
<li><p>Model lifecycle explicitly (<code>draft</code> → <code>submitted</code> → <code>approved</code>) instead of overloading a single <code>status</code> field with undocumented combinations.</p>
</li>
<li><p>Return what changed after mutations (updated resource, relevant IDs, next allowed actions).</p>
</li>
<li><p>Avoid silent coercion (auto-correcting bad enums, silently dropping unknown fields) unless you document and signal it.</p>
</li>
</ul>
<h3 id="heading-make-writes-safe-idempotency-and-intent-keys">Make Writes Safe: Idempotency and Intent Keys</h3>
<p>For any endpoint that bills, sends messages, provisions infrastructure, or otherwise <strong>does something irreversible</strong>, assume double-submission will happen.</p>
<ul>
<li><p>Support idempotency keys (header or body) for create-like operations.</p>
</li>
<li><p>Use clear HTTP semantics: <code>POST</code> creates, <code>PUT</code> replaces where appropriate, <code>PATCH</code> for partial updates and document what repeats mean.</p>
</li>
<li><p>Where duplicates are possible, offer a lookup-by-client-reference path so agents can reconcile.</p>
</li>
</ul>
<h3 id="heading-pagination-and-sorting-one-pattern-everywhere">Pagination and Sorting: One Pattern, Everywhere</h3>
<p>Agents loop. If every resource paginates differently, the model will mix strategies.</p>
<p>To combat this, pick one pagination style (cursor vs offset) per API surface and stick to it.</p>
<p>Also, always return stable sort order or require <code>sort</code> explicitly. You should also include <code>next</code> links or cursors in a consistent envelope.</p>
<h3 id="heading-timeouts-partial-success-and-async-work">Timeouts, Partial Success, and Async Work</h3>
<p>Agents hate “maybe it worked.” Long-running work should be <strong>explicitly async</strong>:</p>
<ul>
<li><p><code>202 Accepted</code> + job ID + polling or webhooks.</p>
</li>
<li><p>Clear terminal states: <code>succeeded</code>, <code>failed</code>, <code>canceled</code>, with structured error details on failure.</p>
</li>
</ul>
<h2 id="heading-principle-2-strong-schemas">Principle 2: Strong Schemas</h2>
<p>If determinism is about behavior, schemas are about communication. For agents, your OpenAPI (or equivalent) isn't paperwork, it's part of the runtime interface.</p>
<h3 id="heading-treat-openapi-as-a-contract-not-a-souvenir">Treat OpenAPI as a Contract, Not a Souvenir</h3>
<p>A specification that lags production is worse than no spec: it trains the agent to be confidently wrong. Teams increasingly treat OpenAPI as the authoritative contract and validate requests/responses against it in CI and at the edge.</p>
<p>Here's the minimum bar for agent-friendly OpenAPI:</p>
<ul>
<li><p>Every operation has a <code>summary</code> and a <code>description</code> that explain <em>when</em> to use it, not only <em>what</em> it returns.</p>
</li>
<li><p>Every request body property has <code>description</code> and realistic <code>example</code> values.</p>
</li>
<li><p>All responses are documented including 4xx/5xx with stable JSON shapes.</p>
</li>
</ul>
<h3 id="heading-describe-intent-in-natural-language-precisely">Describe Intent in Natural Language, Precisely</h3>
<p>Agents aren't offended by verbosity. They're confused by vague verbs.</p>
<p>Instead of:</p>
<blockquote>
<p>“Gets orders.”</p>
</blockquote>
<p>Prefer:</p>
<blockquote>
<p>“Lists orders for the authenticated merchant. Supports filtering by <code>status</code> and a time window on <code>created_at</code>. Returns at most <code>limit</code> items; use <code>cursor</code> for the next page.”</p>
</blockquote>
<p>This aligns with what multiple guides call <strong>context-aware</strong> or <strong>self-describing</strong> APIs: the schema carries semantic intent, not just types.</p>
<h3 id="heading-examples-are-part-of-the-contract">Examples Are Part of the Contract</h3>
<p>You should provide a happy path example per endpoint, at least one validation error example (400) with your standard error object, and examples for optional fields when they change behavior.</p>
<p>Examples reduce “shape hallucination” where the model guesses field names or nesting.</p>
<h3 id="heading-json-schema-strictness-helps-tool-calling-stacks">JSON Schema Strictness Helps Tool-Calling Stacks</h3>
<p>If your agent uses function calling / structured outputs, tighten schemas:</p>
<ul>
<li><p>Prefer <code>enum</code> for small closed sets.</p>
</li>
<li><p>Mark fields <code>required</code> honestly.</p>
</li>
<li><p>Use <code>format</code> (<code>uuid</code>, <code>date-time</code>) where real.</p>
</li>
<li><p>Avoid <code>additionalProperties: true</code> on security-sensitive payloads if you need strict validation.</p>
</li>
</ul>
<h3 id="heading-name-things-consistently">Name Things Consistently</h3>
<p><code>userId</code> in one endpoint and <code>user_id</code> in another is a human annoyance and an agent trap. Pick a convention and enforce it.</p>
<h2 id="heading-principle-3-guardrails-at-the-api-boundary">Principle 3: Guardrails at the API Boundary</h2>
<p>Autonomy amplifies mistakes. Guardrails turn “oops” into blocked requests instead of incidents.</p>
<h3 id="heading-authorization-should-be-narrow-and-explicit">Authorization Should Be Narrow and Explicit</h3>
<p>Agents should receive credentials scoped to <strong>least privilege</strong>. For example, use short-lived tokens, with refresh documented clearly. Use scopes that map to real actions (<code>orders:read</code> vs <code>orders:write</code>). And avoid flows that assume a human can solve (CAPTCHAs) or click (email links mid-run) or isolate those as human-in-the-loop tools.</p>
<h3 id="heading-validate-hard-fail-loud-and-structured">Validate Hard, Fail Loud and Structured</h3>
<p>Reject bad input at the edge with stable <code>error_code</code> values (machine-actionable), human-readable <code>message</code> (for logs and UI), optional <code>field</code> or JSON Pointer to the problem, and optional <code>doc_url</code> linking to documentation.</p>
<p>This matches guidance from several practitioner articles: opaque 500s and generic errors are where autonomous clients spiral.</p>
<p>RFC 7807 Problem Details (<code>application/problem+json</code>) is a good, widely understood pattern for HTTP APIs, a structured envelope agents can parse consistently.</p>
<h3 id="heading-separate-read-the-world-from-change-the-world">Separate “Read the World” from “Change the World”</h3>
<p>For high-impact actions (refunds, deletes, transfers), consider using a two-step pattern: first create an intent, then confirm execution.</p>
<p>Or you can dry-run query parameters / dedicated endpoints that validate without committing.</p>
<p>Also keep in mind that rate limits and quotas tuned for bursty agent behavior and autonomous loops can dwarf human traffic.</p>
<h3 id="heading-observability-is-a-product-feature">Observability is a Product Feature</h3>
<p>Log correlation IDs, surface them in responses where safe, and monitor for retry amplification. An agent that misreads a 409 as “retry forever” becomes a denial-of-wallet attack on your own systems.</p>
<h2 id="heading-patterns-that-bridge-apis-and-agent-runtimes">Patterns That Bridge APIs and Agent Runtimes</h2>
<h3 id="heading-workflow-documentation-sequences-not-just-endpoints">Workflow Documentation: Sequences, Not Just Endpoints</h3>
<p>Agents excel when they can follow a recipe. Document common sequences (“create customer → add payment method → charge”) and consider standards meant for multi-step API flows (such as Arazzo) when your product’s complexity justifies it.</p>
<h3 id="heading-hypermedia-and-next-steps">Hypermedia and “Next Steps”</h3>
<p>Including links to plausible next actions (for example, pagination <code>next</code>, or related resources) reduces improvisation. This is the same spirit as <a href="https://en.wikipedia.org/wiki/HATEOAS">HATEOAS</a>: the response whispers what you can do next, instead of forcing the model to guess URLs.</p>
<h3 id="heading-tool-oriented-surfaces-for-example-mcp">Tool-Oriented Surfaces (For Example, MCP)</h3>
<p>Protocols like the Model Context Protocol (MCP) are gaining traction as a way to expose curated capabilities (“tools”) with schemas agents can bind to directly.</p>
<p>A common pragmatic pattern is not to dump every micro-endpoint as a tool, but to expose coarse-grained tools aligned to user outcomes while keeping your underlying REST API strict and clean.</p>
<p>MCP isn't a substitute for good API design. It's a delivery and discovery layer. Slapping a thin wrapper on a messy API still leaves you with a messy system – it just fails faster in public.</p>
<h3 id="heading-metadata-for-discovery-llmstxt-and-friends">Metadata for Discovery (<code>llms.txt</code> and Friends)</h3>
<p>Some teams publish <code>/llms.txt</code> or similar lightweight discovery files for documentation sites. Treat these as optional signposts, not replacements for OpenAPI.</p>
<p>Ecosystem adoption is still evolving, but the underlying idea is sound: make the canonical machine-readable description easy to find.</p>
<h2 id="heading-a-practical-beforeafter">A Practical Before/After</h2>
<h3 id="heading-weak-pattern-agent-hostile">Weak Pattern (Agent-hostile)</h3>
<pre><code class="language-http">POST /do-stuff
</code></pre>
<p>Response <code>200 OK</code>:</p>
<pre><code class="language-json">{ "ok": true }
</code></pre>
<p>Problems: no idempotency, no structured error, no entity ID, no way to poll, the agent must guess whether “ok” means “created” or “ignored duplicate.”</p>
<h3 id="heading-stronger-pattern-agent-friendly">Stronger Pattern (Agent-friendly)</h3>
<pre><code class="language-http">POST /v1/invoices
Idempotency-Key: 7b3c-...
</code></pre>
<p>Response <code>201 Created</code>:</p>
<pre><code class="language-json">{
  "invoice": {
    "id": "inv_9Qz",
    "status": "draft",
    "total": { "amount": "120.00", "currency": "USD" }
  },
  "links": {
    "finalize": "/v1/invoices/inv_9Qz/finalize"
  }
}
</code></pre>
<p>Conflict response <code>409 Conflict</code> with Problem Details:</p>
<pre><code class="language-json">{
  "type": "https://api.example.com/problems/duplicate-idempotency-key",
  "title": "Duplicate idempotency key",
  "status": 409,
  "detail": "A different request body was sent with the same Idempotency-Key.",
  "error_code": "IDEMPOTENCY_KEY_REUSE_BODY_MISMATCH"
}
</code></pre>
<p>This tells the agent what happened and whether retrying is appropriate.</p>
<h2 id="heading-checklist-is-your-api-agent-ready">Checklist: Is Your API Agent-Ready?</h2>
<ul>
<li><p><strong>Contract</strong>: Published OpenAPI 3.x, validated against real traffic, with rich descriptions and examples.</p>
</li>
<li><p><strong>Determinism</strong>: Documented state machines, consistent pagination, explicit async for long jobs.</p>
</li>
<li><p><strong>Safe writes</strong>: Idempotency for side effects, reconciliation endpoints where needed.</p>
</li>
<li><p><strong>Errors</strong>: Stable codes, structured bodies, documented remediation paths.</p>
</li>
<li><p><strong>Security</strong>: Least-privilege tokens, no “mystery” side doors agents can accidentally hit.</p>
</li>
<li><p><strong>Operations</strong>: Rate limits, bulk endpoints where appropriate, correlation IDs, dashboards for anomalous agent traffic.</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Designing for AI agents is, in most respects, disciplined API design — pushed to the level where machines can rely on your contract without tribal knowledge.</p>
<p>If you remember only three things:</p>
<ol>
<li><p><strong>Be predictable:</strong> in shapes, states, and side effects.</p>
</li>
<li><p><strong>Be explicit:</strong> in schemas, examples, and errors.</p>
</li>
<li><p><strong>Be protective:</strong> validate early, scope narrowly, and make dangerous actions hard to trigger by accident.</p>
</li>
</ol>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ A Developer’s Guide to Lazy Loading in React and Next.js ]]>
                </title>
                <description>
                    <![CDATA[ Large JavaScript bundles can slow down your application. When too much code loads at once, users wait longer for the first paint and pages feel less responsive. Search engines may also rank slower sit ]]>
                </description>
                <link>https://www.freecodecamp.org/news/a-developers-guide-to-lazy-loading-in-react-and-nextjs/</link>
                <guid isPermaLink="false">69dea43f91716f3cfb762c99</guid>
                
                    <category>
                        <![CDATA[ React ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ David Aniebo ]]>
                </dc:creator>
                <pubDate>Tue, 14 Apr 2026 20:31:59 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/9e6d8733-23e7-4dab-8da2-98fbbc1c44e9.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Large JavaScript bundles can slow down your application. When too much code loads at once, users wait longer for the first paint and pages feel less responsive. Search engines may also rank slower sites lower in results.</p>
<p>Lazy loading helps solve this problem by splitting your code into smaller chunks and loading them only when they are needed</p>
<p>This guide walks you through lazy loading in React and Next.js. By the end, you'll know when to use <code>React.lazy</code>, <code>next/dynamic</code>, and <code>Suspense</code>, and you'll have working examples you can copy and adapt to your own projects.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-is-lazy-loading">What is Lazy Loading?</a></p>
</li>
<li><p><a href="#heading-prequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-how-to-use-reactlazy-for-code-splitting">How to Use React.lazy for Code Splitting</a></p>
</li>
<li><p><a href="#heading-how-to-use-suspense-with-reactlazy">How to Use Suspense with React.lazy</a></p>
</li>
<li><p><a href="#heading-how-to-handle-errors-with-error-boundaries">How to Handle Errors with Error Boundaries</a></p>
</li>
<li><p><a href="#heading-how-to-use-nextdynamic-in-nextjs">How to Use next/dynamic in Next.js</a></p>
</li>
<li><p><a href="#heading-reactlazy-vs-nextdynamic-when-to-use-each">React.lazy vs next/dynamic: When to Use Each</a></p>
</li>
<li><p><a href="#heading-real-world-examples">Real-World Examples</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-is-lazy-loading">What is Lazy Loading?</h2>
<p>Lazy loading is a performance technique that defers loading code until it's needed. Instead of loading your entire app at once, you split it into smaller chunks. The browser only downloads a chunk when the user navigates to that route or interacts with that feature.</p>
<p>Benefits include:</p>
<ul>
<li><p><strong>Faster initial load</strong>: Smaller first bundle means quicker time to interactive</p>
</li>
<li><p><strong>Better Core Web Vitals</strong>: Improves Largest Contentful Paint and Total Blocking Time</p>
</li>
<li><p><strong>Lower bandwidth</strong>: Users only download what they use</p>
</li>
</ul>
<p>In React, you achieve this with dynamic imports and <code>React.lazy()</code> or Next.js’s <code>next/dynamic</code>.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you follow along, you should have:</p>
<ul>
<li><p>Basic familiarity with React (components, hooks, state)</p>
</li>
<li><p>Node.js installed (version 18 or later recommended)</p>
</li>
<li><p>A React app (Create React App or Vite) or a Next.js app (for the Next.js examples)</p>
</li>
</ul>
<p>For the React examples, you can use Create React App or Vite. For the Next.js examples, use the App Router (Next.js 13 or later).</p>
<h2 id="heading-how-to-use-reactlazy-for-code-splitting">How to Use <code>React.lazy</code> for Code Splitting</h2>
<p><code>React.lazy()</code> lets you define a component as a dynamic import. React will load that component only when it's first rendered.</p>
<p><code>React.lazy()</code> expects a function that returns a dynamic <code>import()</code>. The imported module must use a default export.</p>
<p>Here's a basic example:</p>
<pre><code class="language-jsx">import { lazy } from 'react';

const HeavyChart = lazy(() =&gt; import('./HeavyChart'));
const AdminDashboard = lazy(() =&gt; import('./AdminDashboard'));

function App() {
  return (
    &lt;div&gt;
      &lt;h1&gt;My App&lt;/h1&gt;
      &lt;HeavyChart /&gt;
      &lt;AdminDashboard /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>If you use named exports, you can map them to a default export:</p>
<pre><code class="language-jsx">const ComponentWithNamedExport = lazy(() =&gt;
  import('./MyComponent').then((module) =&gt; ({
    default: module.NamedComponent,
  }))
);
</code></pre>
<p>You can also name chunks for easier debugging in the browser:</p>
<pre><code class="language-jsx">const HeavyChart = lazy(() =&gt;
  import(/* webpackChunkName: "heavy-chart" */ './HeavyChart')
);
</code></pre>
<p><code>React.lazy()</code> alone isn't enough. You must wrap lazy components in <code>Suspense</code> so React knows what to show while they load.</p>
<h2 id="heading-how-to-use-suspense-with-reactlazy">How to Use <code>Suspense</code> with <code>React.lazy</code></h2>
<p><code>Suspense</code> is a React component that shows a fallback UI while its children are loading. It works with <code>React.lazy()</code> to handle the loading state of dynamically imported components.</p>
<p>Wrap your lazy components in <code>Suspense</code> and provide a <code>fallback</code> prop:</p>
<pre><code class="language-jsx">import { lazy, Suspense } from 'react';

const HeavyChart = lazy(() =&gt; import('./HeavyChart'));
const AdminDashboard = lazy(() =&gt; import('./AdminDashboard'));

function App() {
  return (
    &lt;div&gt;
      &lt;h1&gt;My App&lt;/h1&gt;
      &lt;Suspense fallback={&lt;div&gt;Loading chart...&lt;/div&gt;}&gt;
        &lt;HeavyChart /&gt;
      &lt;/Suspense&gt;
      &lt;Suspense fallback={&lt;div&gt;Loading dashboard...&lt;/div&gt;}&gt;
        &lt;AdminDashboard /&gt;
      &lt;/Suspense&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>You can use a single <code>Suspense</code> boundary for multiple lazy components:</p>
<pre><code class="language-jsx">&lt;Suspense fallback={&lt;div&gt;Loading...&lt;/div&gt;}&gt;
  &lt;HeavyChart /&gt;
  &lt;AdminDashboard /&gt;
&lt;/Suspense&gt;
</code></pre>
<p>A more polished fallback improves perceived performance:</p>
<pre><code class="language-jsx">function LoadingSpinner() {
  return (
    &lt;div className="loading-container"&gt;
      &lt;div className="spinner" /&gt;
      &lt;p&gt;Loading...&lt;/p&gt;
    &lt;/div&gt;
  );
}

&lt;Suspense fallback={&lt;LoadingSpinner /&gt;}&gt;
  &lt;HeavyChart /&gt;
&lt;/Suspense&gt;
</code></pre>
<h2 id="heading-how-to-handle-errors-with-error-boundaries">How to Handle Errors with Error Boundaries</h2>
<p><code>React.lazy()</code> and <code>Suspense</code> don't handle loading errors (for example, network failures or missing chunks). For that, you need an Error Boundary.</p>
<p>Error Boundaries are class components that use <code>componentDidCatch</code> or <code>static getDerivedStateFromError</code> to catch errors in their child tree and render a fallback UI.</p>
<p>Here is a simple Error Boundary:</p>
<pre><code class="language-jsx">import { Component } from 'react';

class ErrorBoundary extends Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Error caught by boundary:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || &lt;div&gt;Something went wrong.&lt;/div&gt;;
    }
    return this.props.children;
  }
}
</code></pre>
<p>Wrap your <code>Suspense</code> boundary with an Error Boundary:</p>
<pre><code class="language-jsx">import { lazy, Suspense } from 'react';
import ErrorBoundary from './ErrorBoundary';

const HeavyChart = lazy(() =&gt; import('./HeavyChart'));

function App() {
  return (
    &lt;ErrorBoundary fallback={&lt;div&gt;Failed to load chart. Please try again.&lt;/div&gt;}&gt;
      &lt;Suspense fallback={&lt;div&gt;Loading chart...&lt;/div&gt;}&gt;
        &lt;HeavyChart /&gt;
      &lt;/Suspense&gt;
    &lt;/ErrorBoundary&gt;
  );
}
</code></pre>
<p>If the chunk fails to load, the Error Boundary catches it and shows your fallback instead of a blank screen or unhandled error.</p>
<h2 id="heading-how-to-use-nextdynamic-in-nextjs">How to Use <code>next/dynamic</code> in Next.js</h2>
<p>Next.js provides <code>next/dynamic</code>, which wraps <code>React.lazy()</code> and <code>Suspense</code> and adds options tailored for Next.js (including Server-Side Rendering).</p>
<p>Basic usage:</p>
<pre><code class="language-jsx">'use client';

import dynamic from 'next/dynamic';

const ComponentA = dynamic(() =&gt; import('../components/A'));
const ComponentB = dynamic(() =&gt; import('../components/B'));

export default function Page() {
  return (
    &lt;div&gt;
      &lt;ComponentA /&gt;
      &lt;ComponentB /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<h3 id="heading-custom-loading-ui">Custom Loading UI</h3>
<p>Use the <code>loading</code> option to show a placeholder while the component loads:</p>
<pre><code class="language-jsx">const HeavyChart = dynamic(() =&gt; import('../components/HeavyChart'), {
  loading: () =&gt; &lt;p&gt;Loading chart...&lt;/p&gt;,
});
</code></pre>
<h3 id="heading-disable-server-side-rendering">Disable Server-Side Rendering</h3>
<p>For components that must run only on the client (for example, those using <code>window</code> or browser-only APIs), set <code>ssr: false</code>:</p>
<pre><code class="language-jsx">const ClientOnlyMap = dynamic(() =&gt; import('../components/Map'), {
  ssr: false,
  loading: () =&gt; &lt;p&gt;Loading map...&lt;/p&gt;,
});
</code></pre>
<p>Note: <code>ssr: false</code> works only for Client Components. Use it inside a <code>'use client'</code> file.</p>
<h3 id="heading-load-on-demand">Load on Demand</h3>
<p>You can load a component only when a condition is met:</p>
<pre><code class="language-jsx">'use client';

import { useState } from 'react';
import dynamic from 'next/dynamic';

const Modal = dynamic(() =&gt; import('../components/Modal'), {
  loading: () =&gt; &lt;p&gt;Opening modal...&lt;/p&gt;,
});

export default function Page() {
  const [showModal, setShowModal] = useState(false);

  return (
    &lt;div&gt;
      &lt;button onClick={() =&gt; setShowModal(true)}&gt;Open Modal&lt;/button&gt;
      {showModal &amp;&amp; &lt;Modal onClose={() =&gt; setShowModal(false)} /&gt;}
    &lt;/div&gt;
  );
}
</code></pre>
<h3 id="heading-named-exports">Named Exports</h3>
<p>For named exports, return the component from the dynamic import:</p>
<pre><code class="language-jsx">const Hello = dynamic(() =&gt;
  import('../components/hello').then((mod) =&gt; mod.Hello)
);
</code></pre>
<h3 id="heading-using-suspense-with-nextdynamic">Using Suspense with next/dynamic</h3>
<p>In React 18+, you can use <code>suspense: true</code> to rely on a parent <code>Suspense</code> boundary instead of the <code>loading</code> option:</p>
<pre><code class="language-jsx">const HeavyChart = dynamic(() =&gt; import('../components/HeavyChart'), {
  suspense: true,
});

// In your component:
&lt;Suspense fallback={&lt;div&gt;Loading...&lt;/div&gt;}&gt;
  &lt;HeavyChart /&gt;
&lt;/Suspense&gt;
</code></pre>
<p>Important: When using <code>suspense: true</code>, you can't use <code>ssr: false</code> or the <code>loading</code> option. Use the <code>Suspense</code> fallback instead.</p>
<h2 id="heading-reactlazy-vs-nextdynamic-when-to-use-each"><code>React.lazy</code> vs <code>next/dynamic</code>: When to Use Each</h2>
<table>
<thead>
<tr>
<th>Feature</th>
<th>React.lazy + Suspense</th>
<th>next/dynamic</th>
</tr>
</thead>
<tbody><tr>
<td><strong>Framework</strong></td>
<td>Any React app (Create React App, Vite, etc.)</td>
<td>Next.js only</td>
</tr>
<tr>
<td><strong>Server-Side Rendering</strong></td>
<td>Not supported</td>
<td>Supported by default</td>
</tr>
<tr>
<td><strong>Disable SSR</strong></td>
<td>N/A</td>
<td><code>ssr: false</code> option</td>
</tr>
<tr>
<td><strong>Loading UI</strong></td>
<td><code>Suspense</code> fallback prop</td>
<td>Built-in <code>loading</code> option</td>
</tr>
<tr>
<td><strong>Error handling</strong></td>
<td>Requires Error Boundary</td>
<td>Requires Error Boundary</td>
</tr>
<tr>
<td><strong>Named exports</strong></td>
<td>Manual <code>.then()</code> mapping</td>
<td>Same <code>.then()</code> pattern</td>
</tr>
<tr>
<td><strong>Suspense mode</strong></td>
<td>Always uses Suspense</td>
<td>Optional via <code>suspense: true</code></td>
</tr>
</tbody></table>
<h3 id="heading-when-to-use-reactlazy">When to Use <code>React.lazy</code></h3>
<ul>
<li><p>You're building a <strong>pure React app</strong> (no Next.js)</p>
</li>
<li><p>You use Create React App, Vite, or a custom Webpack setup</p>
</li>
<li><p>You don't need Server-Side Rendering</p>
</li>
<li><p>You want a simple, framework-agnostic approach</p>
</li>
</ul>
<h3 id="heading-when-to-use-nextdynamic">When to <code>Use next/dynamic</code></h3>
<ul>
<li><p>You're building a <strong>Next.js app</strong></p>
</li>
<li><p>You need SSR for some components and want to disable it for others</p>
</li>
<li><p>You want built-in loading placeholders without manually adding <code>Suspense</code></p>
</li>
<li><p>You want Next.js-specific optimizations and defaults</p>
</li>
</ul>
<h2 id="heading-real-world-examples">Real-World Examples</h2>
<h3 id="heading-example-1-route-based-code-splitting-in-react">Example 1: Route-Based Code Splitting in React</h3>
<p>Split your app by route so each page loads only when the user navigates to it:</p>
<pre><code class="language-jsx">// App.jsx
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import ErrorBoundary from './ErrorBoundary';

const Home = lazy(() =&gt; import('./pages/Home'));
const Dashboard = lazy(() =&gt; import('./pages/Dashboard'));
const Settings = lazy(() =&gt; import('./pages/Settings'));

function App() {
  return (
    &lt;BrowserRouter&gt;
      &lt;ErrorBoundary fallback={&lt;div&gt;Failed to load page.&lt;/div&gt;}&gt;
        &lt;Suspense fallback={&lt;div&gt;Loading page...&lt;/div&gt;}&gt;
          &lt;Routes&gt;
            &lt;Route path="/" element={&lt;Home /&gt;} /&gt;
            &lt;Route path="/dashboard" element={&lt;Dashboard /&gt;} /&gt;
            &lt;Route path="/settings" element={&lt;Settings /&gt;} /&gt;
          &lt;/Routes&gt;
        &lt;/Suspense&gt;
      &lt;/ErrorBoundary&gt;
    &lt;/BrowserRouter&gt;
  );
}
</code></pre>
<h3 id="heading-example-2-lazy-loading-a-heavy-chart-library-in-nextjs">Example 2: Lazy Loading a Heavy Chart Library in Next.js</h3>
<p>Defer loading a chart library until the user opens the analytics section:</p>
<pre><code class="language-jsx">// app/analytics/page.jsx
'use client';

import { useState } from 'react';
import dynamic from 'next/dynamic';

const Chart = dynamic(() =&gt; import('../components/Chart'), {
  ssr: false,
  loading: () =&gt; (
    &lt;div className="chart-skeleton"&gt;
      &lt;div className="skeleton-bar" /&gt;
      &lt;div className="skeleton-bar" /&gt;
      &lt;div className="skeleton-bar" /&gt;
    &lt;/div&gt;
  ),
});

export default function AnalyticsPage() {
  const [showChart, setShowChart] = useState(false);

  return (
    &lt;div&gt;
      &lt;h1&gt;Analytics&lt;/h1&gt;
      &lt;button onClick={() =&gt; setShowChart(true)}&gt;Load Chart&lt;/button&gt;
      {showChart &amp;&amp; &lt;Chart /&gt;}
    &lt;/div&gt;
  );
}
</code></pre>
<h3 id="heading-example-3-lazy-loading-a-modal">Example 3: Lazy Loading a Modal</h3>
<p>Load a modal component only when the user clicks to open it:</p>
<pre><code class="language-jsx">// React (with React.lazy)
import { lazy, Suspense, useState } from 'react';

const Modal = lazy(() =&gt; import('./Modal'));

function ProductPage() {
  const [showModal, setShowModal] = useState(false);

  return (
    &lt;div&gt;
      &lt;button onClick={() =&gt; setShowModal(true)}&gt;Add to Cart&lt;/button&gt;
      {showModal &amp;&amp; (
        &lt;Suspense fallback={null}&gt;
          &lt;Modal onClose={() =&gt; setShowModal(false)} /&gt;
        &lt;/Suspense&gt;
      )}
    &lt;/div&gt;
  );
}
</code></pre>
<pre><code class="language-jsx">// Next.js (with next/dynamic)
'use client';

import { useState } from 'react';
import dynamic from 'next/dynamic';

const Modal = dynamic(() =&gt; import('./Modal'), {
  loading: () =&gt; null,
});

export default function ProductPage() {
  const [showModal, setShowModal] = useState(false);

  return (
    &lt;div&gt;
      &lt;button onClick={() =&gt; setShowModal(true)}&gt;Add to Cart&lt;/button&gt;
      {showModal &amp;&amp; &lt;Modal onClose={() =&gt; setShowModal(false)} /&gt;}
    &lt;/div&gt;
  );
}
</code></pre>
<h3 id="heading-example-4-lazy-loading-external-libraries">Example 4: Lazy Loading External Libraries</h3>
<p>Load a library only when the user needs it (for example, when they start typing in a search box):</p>
<pre><code class="language-jsx">'use client';

import { useState } from 'react';

const names = ['Alice', 'Bob', 'Charlie', 'Diana'];

export default function SearchPage() {
  const [results, setResults] = useState([]);
  const [query, setQuery] = useState('');

  const handleSearch = async (value) =&gt; {
    setQuery(value);
    if (!value) {
      setResults([]);
      return;
    }
    // Load fuse.js only when user searches
    const Fuse = (await import('fuse.js')).default;
    const fuse = new Fuse(names);
    setResults(fuse.search(value));
  };

  return (
    &lt;div&gt;
      &lt;input
        type="text"
        placeholder="Search..."
        value={query}
        onChange={(e) =&gt; handleSearch(e.target.value)}
      /&gt;
      &lt;ul&gt;
        {results.map((result) =&gt; (
          &lt;li key={result.refIndex}&gt;{result.item}&lt;/li&gt;
        ))}
      &lt;/ul&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Lazy loading improves performance by splitting your bundle and loading code only when needed. Here's what you learned:</p>
<ul>
<li><p><strong>React.lazy()</strong> – Use in plain React apps for code splitting. It requires a default export and works with dynamic <code>import()</code>.</p>
</li>
<li><p><strong>Suspense</strong> – Wrap lazy components in <code>Suspense</code> and provide a <code>fallback</code> for the loading state.</p>
</li>
<li><p><strong>Error Boundaries</strong> – Use them to catch chunk load failures and show a friendly error UI.</p>
</li>
<li><p><strong>next/dynamic</strong> – Use in Next.js for the same benefits plus SSR control and built-in loading options.</p>
</li>
</ul>
<p>Choose <code>React.lazy</code> for React-only projects and <code>next/dynamic</code> for Next.js. Combine them with <code>Suspense</code> and Error Boundaries for a solid lazy-loading setup.</p>
<p>Start by identifying your heaviest components (charts, modals, admin panels) and lazy load them. Measure your bundle size and Core Web Vitals before and after to see the impact.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Share Components Between Server and Client in NextJS ]]>
                </title>
                <description>
                    <![CDATA[ Next.js App Router splits your app into Server Components and Client Components. Server Components run on the server and keep secrets safe. Client Components run in the browser and handle interactivit ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-share-components-between-server-and-client-in-nextjs/</link>
                <guid isPermaLink="false">69c6eb7c7cf27065104ba964</guid>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ components ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ David Aniebo ]]>
                </dc:creator>
                <pubDate>Fri, 27 Mar 2026 20:41:32 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/80c9b6dc-daaa-49d5-8c3a-f61bff3b7e11.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Next.js App Router splits your app into Server Components and Client Components. Server Components run on the server and keep secrets safe. Client Components run in the browser and handle interactivity. The challenge is sharing data and UI between them without breaking the rules of each environment.</p>
<p>This guide shows you how to share components and data between Server and Client Components in Next.js. You'll learn composition patterns, prop passing rules, and when to use each approach.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-what-are-server-and-client-components">What are Server and Client Components?</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-how-to-pass-data-from-server-to-client-via-props">How to Pass Data from Server to Client via Props</a></p>
</li>
<li><p><a href="#heading-how-to-pass-server-components-as-children-to-client-components">How to Pass Server Components as Children to Client Components</a></p>
</li>
<li><p><a href="#heading-what-props-are-allowed-between-server-and-client">What Props Are Allowed Between Server and Client</a></p>
</li>
<li><p><a href="#heading-how-to-share-data-with-context-and-reactcache">How to Share Data with Context and React.cache</a></p>
</li>
<li><p><a href="#heading-how-to-use-third-party-components-in-both-environments">How to Use Third-Party Components in Both Environments</a></p>
</li>
<li><p><a href="#heading-how-to-prevent-environment-poisoning-with-server-only-and-client-only">How to Prevent Environment Poisoning with server-only and client-only</a></p>
</li>
<li><p><a href="#heading-real-world-examples">Real-World Examples</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-are-server-and-client-components">What are Server and Client Components?</h2>
<p>In the Next.js App Router, every component is a Server Component by default. Server Components run only on the server. They can fetch data from databases, use API keys, and keep sensitive logic out of the browser. They don't send JavaScript to the client, which reduces bundle size.</p>
<p>Client Components run on both the server (for the initial HTML) and the client (for interactivity). You mark them with the <code>"use client"</code> directive at the top of the file. They can use <code>useState</code>, <code>useEffect</code>, event handlers, and browser APIs like <code>localStorage</code> and <code>window</code>.</p>
<p>The key rule: <strong>Server Components can import and render Client Components, but Client Components can't import Server Components directly.</strong> They can only receive them as props (such as <code>children</code>).</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you follow along, you should have:</p>
<ul>
<li><p>Basic familiarity with React (components, props, hooks)</p>
</li>
<li><p>A Next.js project using the App Router (Next.js 13 or later)</p>
</li>
<li><p>Node.js installed (version 18 or later recommended)</p>
</li>
</ul>
<p>If you don't have a Next.js project yet, create one with:</p>
<pre><code class="language-bash">npx create-next-app@latest my-app
</code></pre>
<h2 id="heading-how-to-pass-data-from-server-to-client-via-props">How to Pass Data from Server to Client via Props</h2>
<p>The simplest way to share data between Server and Client Components is to pass it as props. The Server Component fetches the data, and the Client Component receives it and handles interactivity.</p>
<p>Here is a basic example. A page (Server Component) fetches a post and passes the like count to a <code>LikeButton</code> (Client Component):</p>
<pre><code class="language-jsx">// app/post/[id]/page.jsx (Server Component)
import LikeButton from '@/app/ui/like-button';
import { getPost } from '@/lib/data';

export default async function PostPage({ params }) {
  const { id } = await params;
  const post = await getPost(id);

  return (
    &lt;div&gt;
      &lt;h1&gt;{post.title}&lt;/h1&gt;
      &lt;p&gt;{post.content}&lt;/p&gt;
      &lt;LikeButton likes={post.likes} postId={post.id} /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<pre><code class="language-jsx">// app/ui/like-button.jsx (Client Component)
'use client';

import { useState } from 'react';

export default function LikeButton({ likes, postId }) {
  const [count, setCount] = useState(likes);

  const handleLike = () =&gt; {
    setCount((c) =&gt; c + 1);
    // Call API or Server Action to persist
  };

  return (
    &lt;button onClick={handleLike}&gt;
      {count} likes
    &lt;/button&gt;
  );
}
</code></pre>
<p>The Server Component fetches data on the server. The Client Component receives plain values (<code>likes</code>, <code>postId</code>) and manages state and events. This pattern keeps data fetching on the server and interactivity on the client.</p>
<h2 id="heading-how-to-pass-server-components-as-children-to-client-components">How to Pass Server Components as Children to Client Components</h2>
<p>You can pass a Server Component as the <code>children</code> prop (or any prop) to a Client Component. The Server Component still renders on the server. The Client Component receives the rendered output, not the component code.</p>
<p>This is useful when you want a Client Component to wrap or control the layout of server-rendered content. For example, a modal that shows server-fetched data:</p>
<pre><code class="language-jsx">// app/ui/modal.jsx (Client Component)
'use client';

import { useState } from 'react';

export default function Modal({ children }) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    &lt;&gt;
      &lt;button onClick={() =&gt; setIsOpen(true)}&gt;Open&lt;/button&gt;
      {isOpen &amp;&amp; (
        &lt;div className="modal-overlay"&gt;
          &lt;div className="modal-content"&gt;{children}&lt;/div&gt;
        &lt;/div&gt;
      )}
    &lt;/&gt;
  );
}
</code></pre>
<pre><code class="language-jsx">// app/cart/page.jsx (Server Component)
import Modal from '@/app/ui/modal';
import Cart from '@/app/ui/cart';

export default function CartPage() {
  return (
    &lt;Modal&gt;
      &lt;Cart /&gt;
    &lt;/Modal&gt;
  );
}
</code></pre>
<pre><code class="language-jsx">// app/ui/cart.jsx (Server Component - no 'use client')
import { getCart } from '@/lib/cart';

export default async function Cart() {
  const items = await getCart();

  return (
    &lt;ul&gt;
      {items.map((item) =&gt; (
        &lt;li key={item.id}&gt;{item.name}&lt;/li&gt;
      ))}
    &lt;/ul&gt;
  );
}
</code></pre>
<p><code>Cart</code> is a Server Component that fetches cart data. It's passed as <code>children</code> to <code>Modal</code>, which is a Client Component. The server renders <code>Cart</code> first. The RSC Payload includes the rendered result. The client receives that output and displays it inside the modal. The cart data never runs on the client.</p>
<p>You can use the same pattern with named props (slots):</p>
<pre><code class="language-jsx">// app/ui/tabs.jsx (Client Component)
'use client';

import { useState } from 'react';

export default function Tabs({ tabs, children }) {
  const [activeIndex, setActiveIndex] = useState(0);

  return (
    &lt;div&gt;
      &lt;div className="tab-list"&gt;
        {tabs.map((tab, i) =&gt; (
          &lt;button
            key={tab.id}
            onClick={() =&gt; setActiveIndex(i)}
            className={activeIndex === i ? 'active' : ''}
          &gt;
            {tab.label}
          &lt;/button&gt;
        ))}
      &lt;/div&gt;
      &lt;div className="tab-content"&gt;{children[activeIndex]}&lt;/div&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<pre><code class="language-jsx">// app/dashboard/page.jsx (Server Component)
import Tabs from '@/app/ui/tabs';
import Overview from '@/app/ui/overview';
import Analytics from '@/app/ui/analytics';

export default function DashboardPage() {
  const tabs = [
    { id: 'overview', label: 'Overview' },
    { id: 'analytics', label: 'Analytics' },
  ];

  return (
    &lt;Tabs tabs={tabs}&gt;
      &lt;Overview /&gt;
      &lt;Analytics /&gt;
    &lt;/Tabs&gt;
  );
}
</code></pre>
<p><code>Overview</code> and <code>Analytics</code> can be Server Components that fetch their own data. They render on the server, and the client receives the pre-rendered output.</p>
<h2 id="heading-what-props-are-allowed-between-server-and-client">What Props Are Allowed Between Server and Client</h2>
<p>Props passed from Server Components to Client Components must be <strong>serializable</strong>. React serializes them into the RSC Payload so they can be sent to the client.</p>
<h3 id="heading-allowed-types">Allowed Types</h3>
<ul>
<li><p>Strings, numbers, booleans</p>
</li>
<li><p><code>null</code> and <code>undefined</code></p>
</li>
<li><p>Plain objects (no functions, no class instances)</p>
</li>
<li><p>Arrays of serializable values</p>
</li>
<li><p>JSX (Server Components as children or other props)</p>
</li>
<li><p>Server Actions (functions with <code>"use server"</code>)</p>
</li>
</ul>
<h3 id="heading-not-allowed">Not Allowed</h3>
<ul>
<li><p>Functions (except Server Actions)</p>
</li>
<li><p><code>Date</code> objects</p>
</li>
<li><p>Class instances</p>
</li>
<li><p>Symbols</p>
</li>
<li><p><code>Map</code>, <code>Set</code>, <code>WeakMap</code>, <code>WeakSet</code></p>
</li>
<li><p>Objects with custom prototypes</p>
</li>
<li><p>Buffers, <code>ArrayBuffer</code>, typed arrays</p>
</li>
</ul>
<p>If you need to pass a <code>Date</code>, convert it to a string or number first:</p>
<pre><code class="language-jsx">&lt;ClientComponent createdAt={post.createdAt.toISOString()} /&gt;
</code></pre>
<p>If you use MongoDB, convert <code>ObjectId</code> to a string:</p>
<pre><code class="language-jsx">&lt;PostThread userId={user._id.toString()} /&gt;
</code></pre>
<h3 id="heading-passing-server-actions-as-props">Passing Server Actions as props</h3>
<p>Server Actions are async functions marked with <code>"use server"</code>. You can pass them as props to Client Components. They are serialized by reference, not by value.</p>
<pre><code class="language-jsx">// app/actions/post.js
'use server';

export async function likePost(postId) {
  // Update database
  revalidatePath(`/post/${postId}`);
}
</code></pre>
<pre><code class="language-jsx">// app/post/[id]/page.jsx (Server Component)
import LikeButton from '@/app/ui/like-button';
import { likePost } from '@/app/actions/post';

export default async function PostPage({ params }) {
  const post = await getPost((await params).id);

  return &lt;LikeButton likes={post.likes} postId={post.id} onLike={likePost} /&gt;;
}
</code></pre>
<pre><code class="language-jsx">// app/ui/like-button.jsx (Client Component)
'use client';

export default function LikeButton({ likes, postId, onLike }) {
  const handleClick = () =&gt; {
    onLike(postId);
  };

  return &lt;button onClick={handleClick}&gt;{likes} likes&lt;/button&gt;;
}
</code></pre>
<p>You can also bind arguments:</p>
<pre><code class="language-jsx">&lt;LikeButton onLike={likePost.bind(null, post.id)} /&gt;
</code></pre>
<h2 id="heading-how-to-share-data-with-context-and-reactcache">How to Share Data with Context and React.cache</h2>
<p>React Context doesn't work in Server Components. To share data between Server and Client Components, you can combine a Client Component context provider with <code>React.cache</code> for server-side memoization.</p>
<p>Create a cached fetch function:</p>
<pre><code class="language-js">// lib/user.js
import { cache } from 'react';

export const getUser = cache(async () =&gt; {
  const res = await fetch('https://api.example.com/user');
  return res.json();
});
</code></pre>
<p>Create a provider that accepts a promise and stores it in context:</p>
<pre><code class="language-jsx">// app/providers/user-provider.jsx
'use client';

import { createContext } from 'react';

export const UserContext = createContext(null);

export default function UserProvider({ children, userPromise }) {
  return (
    &lt;UserContext.Provider value={userPromise}&gt;
      {children}
    &lt;/UserContext.Provider&gt;
  );
}
</code></pre>
<p>In your root layout, pass the promise without awaiting it:</p>
<pre><code class="language-jsx">// app/layout.jsx
import UserProvider from '@/app/providers/user-provider';
import { getUser } from '@/lib/user';

export default function RootLayout({ children }) {
  const userPromise = getUser();

  return (
    &lt;html&gt;
      &lt;body&gt;
        &lt;UserProvider userPromise={userPromise}&gt;{children}&lt;/UserProvider&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}
</code></pre>
<p>Client Components use <code>use()</code> to unwrap the promise:</p>
<pre><code class="language-jsx">// app/ui/profile.jsx
'use client';

import { use, useContext } from 'react';
import { UserContext } from '@/app/providers/user-provider';

export default function Profile() {
  const userPromise = useContext(UserContext);
  if (!userPromise) {
    throw new Error('Profile must be used within UserProvider');
  }
  const user = use(userPromise);

  return &lt;p&gt;Welcome, {user.name}&lt;/p&gt;;
}
</code></pre>
<p>Wrap the component in <code>Suspense</code> for loading states:</p>
<pre><code class="language-jsx">// app/dashboard/page.jsx
import { Suspense } from 'react';
import Profile from '@/app/ui/profile';

export default function DashboardPage() {
  return (
    &lt;Suspense fallback={&lt;div&gt;Loading profile...&lt;/div&gt;}&gt;
      &lt;Profile /&gt;
    &lt;/Suspense&gt;
  );
}
</code></pre>
<p>Server Components can call <code>getUser()</code> directly. Because it's wrapped in <code>React.cache</code>, multiple calls in the same request return the same result:</p>
<pre><code class="language-jsx">// app/settings/page.jsx
import { getUser } from '@/lib/user';

export default async function SettingsPage() {
  const user = await getUser();
  return &lt;h1&gt;Settings for {user.name}&lt;/h1&gt;;
}
</code></pre>
<p><code>React.cache</code> is scoped per request. Each request has its own memoization – there's no sharing across requests.</p>
<h2 id="heading-how-to-use-third-party-components-in-both-environments">How to Use Third-Party Components in Both Environments</h2>
<p>Some third-party components use <code>useState</code>, <code>useEffect</code>, or browser APIs but don't have <code>"use client"</code> in their source. If you use them in a Server Component, you'll get an error.</p>
<p>Wrap them in your own Client Component:</p>
<pre><code class="language-jsx">// app/ui/carousel-wrapper.jsx
'use client';

import { Carousel } from 'acme-carousel';

export default Carousel;
</code></pre>
<p>Now you can use it in a Server Component:</p>
<pre><code class="language-jsx">// app/gallery/page.jsx
import Carousel from '@/app/ui/carousel-wrapper';

export default function GalleryPage() {
  return (
    &lt;div&gt;
      &lt;h1&gt;Gallery&lt;/h1&gt;
      &lt;Carousel /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>If the third-party component is already used inside a Client Component, you don't need a wrapper. The parent's <code>"use client"</code> boundary is enough:</p>
<pre><code class="language-jsx">'use client';

import { Carousel } from 'acme-carousel';

export default function Gallery() {
  return &lt;Carousel /&gt;;
}
</code></pre>
<h2 id="heading-how-to-prevent-environment-poisoning-with-server-only-and-client-only">How to Prevent Environment Poisoning with server-only and client-only</h2>
<p>It's easy to accidentally import server-only code (database clients, API keys) into a Client Component. To catch this at build time, use the <code>server-only</code> package.</p>
<p>Install it:</p>
<pre><code class="language-bash">npm install server-only
</code></pre>
<p>Add it at the top of files that must never run on the client:</p>
<pre><code class="language-js">// lib/data.js
import 'server-only';

export async function getSecretData() {
  const res = await fetch('https://api.example.com/data', {
    headers: {
      authorization: process.env.API_KEY,
    },
  });
  return res.json();
}
</code></pre>
<p>If you import <code>getSecretData</code> (or any symbol from this file) into a Client Component, the build will fail with a clear error.</p>
<p>For client-only code (for example, code that uses <code>window</code>), use <code>client-only</code>:</p>
<pre><code class="language-bash">npm install client-only
</code></pre>
<pre><code class="language-typescript">// lib/analytics.js
import 'client-only';

export function trackEvent(name) {
  if (typeof window !== 'undefined') {
    window.analytics?.track(name);
  }
}
</code></pre>
<p>Importing this into a Server Component will cause a build error.</p>
<h2 id="heading-real-world-examples">Real-World Examples</h2>
<h3 id="heading-example-1-layout-with-shared-server-and-client-pieces">Example 1: Layout with Shared Server and Client Pieces</h3>
<p>Keep the layout as a Server Component. Only the interactive parts are Client Components:</p>
<pre><code class="language-jsx">// app/layout.jsx (Server Component)
import Logo from '@/app/ui/logo';
import Search from '@/app/ui/search';

export default function Layout({ children }) {
  return (
    &lt;html&gt;
      &lt;body&gt;
        &lt;nav&gt;
          &lt;Logo /&gt;
          &lt;Search /&gt;
        &lt;/nav&gt;
        &lt;main&gt;{children}&lt;/main&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}
</code></pre>
<pre><code class="language-jsx">// app/ui/logo.jsx (Server Component - no directive)
export default function Logo() {
  return &lt;img src="/logo.svg" alt="Logo" /&gt;;
}
</code></pre>
<pre><code class="language-jsx">// app/ui/search.jsx (Client Component)
'use client';

import { useState } from 'react';

export default function Search() {
  const [query, setQuery] = useState('');

  return (
    &lt;input
      type="search"
      value={query}
      onChange={(e) =&gt; setQuery(e.target.value)}
    /&gt;
  );
}
</code></pre>
<p><code>Logo</code> stays on the server. <code>Search</code> is interactive and runs on the client. The layout composes both.</p>
<h3 id="heading-example-2-product-page-with-server-data-and-client-add-to-cart">Example 2: Product Page with Server Data and Client Add-to-Cart</h3>
<pre><code class="language-jsx">// app/product/[id]/page.jsx (Server Component)
import { getProduct } from '@/lib/products';
import AddToCartButton from '@/app/ui/add-to-cart-button';

export default async function ProductPage({ params }) {
  const { id } = await params;
  const product = await getProduct(id);

  return (
    &lt;div&gt;
      &lt;h1&gt;{product.name}&lt;/h1&gt;
      &lt;p&gt;{product.description}&lt;/p&gt;
      &lt;p&gt;${product.price}&lt;/p&gt;
      &lt;AddToCartButton productId={product.id} price={product.price} /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<pre><code class="language-jsx">// app/ui/add-to-cart-button.jsx (Client Component)
'use client';

import { useState } from 'react';

export default function AddToCartButton({ productId, price }) {
  const [added, setAdded] = useState(false);

  const handleClick = () =&gt; {
    // Call Server Action or API
    setAdded(true);
  };

  return (
    &lt;button onClick={handleClick} disabled={added}&gt;
      {added ? 'Added!' : `Add to Cart - $${price}`}
    &lt;/button&gt;
  );
}
</code></pre>
<h3 id="heading-example-3-theme-provider-wrapping-server-content">Example 3: Theme Provider Wrapping Server Content</h3>
<pre><code class="language-jsx">// app/providers/theme-provider.jsx (Client Component)
'use client';

import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext('light');

export function useTheme() {
  return useContext(ThemeContext);
}

export default function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  return (
    &lt;ThemeContext.Provider value={{ theme, setTheme }}&gt;
      {children}
    &lt;/ThemeContext.Provider&gt;
  );
}
</code></pre>
<pre><code class="language-jsx">// app/layout.jsx (Server Component)
import ThemeProvider from '@/app/providers/theme-provider';

export default function RootLayout({ children }) {
  return (
    &lt;html&gt;
      &lt;body&gt;
        &lt;ThemeProvider&gt;{children}&lt;/ThemeProvider&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}
</code></pre>
<p><code>ThemeProvider</code> is a Client Component. It wraps <code>children</code>, which can be Server Components. Render providers as deep as possible in the tree so Next.js can optimize static Server Components.</p>
<h3 id="heading-example-4-shared-utility-without-directives">Example 4: Shared Utility Without Directives</h3>
<p>Pure utilities (no hooks, no browser APIs) can be shared. They run in the environment of the component that imports them:</p>
<pre><code class="language-js">// lib/format.js (shared - no directive)
export function formatPrice(amount) {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
  }).format(amount);
}
</code></pre>
<pre><code class="language-jsx">// Server Component
import { formatPrice } from '@/lib/format';

export default async function ProductPage() {
  const product = await getProduct();
  return &lt;p&gt;{formatPrice(product.price)}&lt;/p&gt;;
}
</code></pre>
<pre><code class="language-jsx">// Client Component
'use client';

import { formatPrice } from '@/lib/format';

export default function PriceDisplay({ amount }) {
  return &lt;span&gt;{formatPrice(amount)}&lt;/span&gt;;
}
</code></pre>
<p><code>formatPrice</code> is a pure function. It works in both Server and Client Components.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Sharing components and data between Server and Client Components in Next.js comes down to a few patterns:</p>
<ul>
<li><p><strong>Pass data as props</strong> – Server Components fetch data and pass serializable values to Client Components.</p>
</li>
<li><p><strong>Pass Server Components as children</strong> – Client Components can wrap server-rendered content via <code>children</code> or slot props.</p>
</li>
<li><p><strong>Use serializable props only</strong> – Stick to primitives, plain objects, arrays, and Server Actions. Convert <code>Date</code> and <code>ObjectId</code> to strings.</p>
</li>
<li><p><strong>Share data with Context + React.cache</strong> – Use a Client Component provider that receives a promise and <code>React.cache</code> for server-side deduplication.</p>
</li>
<li><p><strong>Wrap third-party components</strong> – Add <code>"use client"</code> wrappers for libraries that use client-only features.</p>
</li>
<li><p><strong>Use server-only and client-only</strong> – Prevent accidental imports across the server/client boundary.</p>
</li>
</ul>
<p>Keep Server Components at the top of the tree and push Client Components down to leaf nodes. This reduces JavaScript sent to the browser while keeping interactivity where you need it.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build Real-Time Update Systems with MQTT and Express.js  ]]>
                </title>
                <description>
                    <![CDATA[ Real-time updates are everywhere – like live sports scores, stock tickers, chat applications, and IoT dashboards. If you want to build systems that push data to users the moment it changes, you need t ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-real-time-update-systems-with-mosquitto-and-expressjs/</link>
                <guid isPermaLink="false">69b04145abc0d950017e4629</guid>
                
                    <category>
                        <![CDATA[ Express ]]>
                    </category>
                
                    <category>
                        <![CDATA[ mqtt ]]>
                    </category>
                
                    <category>
                        <![CDATA[ backend ]]>
                    </category>
                
                    <category>
                        <![CDATA[ web application ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ David Aniebo ]]>
                </dc:creator>
                <pubDate>Tue, 10 Mar 2026 16:05:25 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/f39f54d7-d39b-46aa-9046-9a88315369d4.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Real-time updates are everywhere – like live sports scores, stock tickers, chat applications, and IoT dashboards. If you want to build systems that push data to users the moment it changes, you need the right tools.</p>
<p>Message Queuing Telemetry Transport (MQTT) is a lightweight messaging protocol that excels at this. Combined with a broker like Mosquitto and a web framework like Express, you can build a production-ready real-time system in a single afternoon.</p>
<p>In this tutorial, you'll build a complete real-time football (soccer) sports update system from scratch. You'll create an admin interface for uploading scores and match details, a viewer interface for watching live updates, and a backend that uses MQTT to broadcast changes instantly to every connected client.</p>
<p>By the end of this guide, you'll understand how to integrate MQTT with Express, set up the Mosquitto broker, and deliver real-time data to web browsers using Server-Sent Events. You will have a working system that you can extend for production use.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-what-you-will-learn">What You Will Learn</a></p>
</li>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-understanding-the-architecture">Understanding the Architecture</a></p>
</li>
<li><p><a href="#heading-what-is-mqtt-and-why-use-it">What is MQTT and Why Use It?</a></p>
<ul>
<li><p><a href="#heading-mqtt-topic-design">MQTT Topic Design</a></p>
</li>
<li><p><a href="#heading-why-serversent-events-instead-of-websockets">Why Server-Sent Events Instead of WebSockets?</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a href="#heading-how-to-set-up-the-mqtt-broker">How to Set Up the MQTT Broker</a></p>
<ul>
<li><p><a href="#heading-option-1-docker-recommended">Option 1: Docker (Recommended)</a></p>
</li>
<li><p><a href="#heading-option-2-local-install">Option 2: Local Install</a></p>
</li>
<li><p><a href="#heading-option-3-public-test-broker">Option 3: Public Test Broker</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-how-to-build-the-express-server">How to Build the Express Server</a></p>
</li>
<li><p><a href="#heading-how-to-implement-the-match-routes">How to Implement the Match Routes</a></p>
</li>
<li><p><a href="#heading-how-to-bridge-mqtt-to-the-browser-with-serversent-events">How to Bridge MQTT to the Browser with Server-Sent Events</a></p>
</li>
<li><p><a href="#heading-how-to-build-the-admin-upload-interface">How to Build the Admin Upload Interface</a></p>
<ul>
<li><p><a href="#heading-admin-html-structure-and-styles">Admin HTML Structure and Styles</a></p>
</li>
<li><p><a href="#heading-admin-javascript-logic">Admin JavaScript Logic</a></p>
</li>
</ul>
</li>
<li><p><a href="#heading-how-to-build-the-live-viewer-interface">How to Build the Live Viewer Interface</a></p>
<ul>
<li><a href="#heading-viewer-javascript-logic">Viewer JavaScript Logic</a></li>
</ul>
</li>
<li><p><a href="#heading-how-to-build-the-home-page">How to Build the Home Page</a></p>
</li>
<li><p><a href="#heading-how-to-run-and-test-the-system">How to Run and Test the System</a></p>
</li>
<li><p><a href="#heading-how-to-extend-the-system-for-production">How to Extend the System for Production</a></p>
</li>
<li><p><a href="#heading-api-reference">API Reference</a></p>
</li>
<li><p><a href="#heading-troubleshooting">Troubleshooting</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ol>
<h2 id="heading-what-you-will-learn">What You Will Learn</h2>
<p>During this tutorial, you'll learn how to:</p>
<ul>
<li><p>Connect an Express server to an MQTT broker using the MQTT.js library</p>
</li>
<li><p>Publish and subscribe to MQTT topics for real-time messaging</p>
</li>
<li><p>Use Server-Sent Events to push MQTT messages to web browsers</p>
</li>
<li><p>Build a REST application programming interface (API) for match and score management</p>
</li>
<li><p>Create a simple admin interface for uploading match data</p>
</li>
<li><p>Create a viewer interface that updates in real time without page refreshes</p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you start, you should have:</p>
<ul>
<li><p>Node.js version 18 or later installed on your machine</p>
</li>
<li><p>Basic familiarity with JavaScript, Express, and HTML</p>
</li>
<li><p>A terminal or command line for running commands</p>
</li>
<li><p>Docker installed (optional, for running Mosquitto in a container)</p>
</li>
</ul>
<p>If you don't have Node.js installed, you can download it from the official Node.js website.</p>
<h2 id="heading-understanding-the-architecture">Understanding the Architecture</h2>
<p>The system has three main parts:</p>
<ol>
<li><p><strong>Admin interface</strong> – A web page where you create matches, update scores, and add events such as goals and cards.</p>
</li>
<li><p><strong>Express server</strong> – Receives HyperText Transfer Protocol (HTTP) requests from the admin, publishes data to MQTT, subscribes to MQTT topics, and streams updates to viewers via Server-Sent Events.</p>
</li>
<li><p><strong>Viewer interface</strong> – A web page that connects to the server and displays live scores and events as they arrive.</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/6904c2dbd42ef6b1f9e61c3e/e6d0b007-764d-41bf-8d05-9e37971bd78e.png" alt="Architecture-diagram" style="display:block;margin:0 auto" width="2100" height="890" loading="lazy">

<p><strong>How the flow works</strong><br>When you submit a score update in the admin panel, the Express server publishes a message to the MQTT broker. The server also subscribes to those same topics. When a message arrives, the server forwards it to all connected viewers through Server-Sent Events. The viewers update their display without refreshing the page.</p>
<h2 id="heading-what-is-mqtt-and-why-use-it">What is MQTT and Why Use It?</h2>
<p>MQTT stands for Message Queuing Telemetry Transport. It's a lightweight, publish-subscribe messaging protocol designed for low bandwidth and unreliable networks. It's widely used in Internet of Things (IoT) applications, but it works well for any real-time system where you need to broadcast updates to many subscribers.</p>
<p>Here are some reasons to use MQTT for a sports update system:</p>
<ul>
<li><p><strong>Low overhead</strong>: Messages are small and efficient, which helps when you have many clients.</p>
</li>
<li><p><strong>Built-in Quality of Service (QoS)</strong>: You can choose how many times a message is delivered (at most once, at least once, or exactly once).</p>
</li>
<li><p><strong>Topic-based routing</strong>: You organize messages by topic (for example, <code>sports/football/match/123</code>) so subscribers receive only what they need.</p>
</li>
<li><p><strong>Broker-based</strong>: A central broker (Mosquitto) handles all message distribution, so your application logic stays simple.</p>
</li>
</ul>
<p>Mosquitto is a popular, open-source MQTT broker that's easy to install and configure.</p>
<h3 id="heading-mqtt-topic-design">MQTT Topic Design</h3>
<p>MQTT uses a hierarchical topic structure. For this project, the topics are:</p>
<ul>
<li><p><code>sports/football/match/{id}</code>: One topic per match. When you publish the full match object here, any subscriber receives the complete state. This makes it easy to add new match fields later without changing the topic structure.</p>
</li>
<li><p><code>sports/football/scores</code>: A single topic for score-related notifications. Messages include a <code>type</code> field (<code>match_created</code> or <code>score_update</code>) so subscribers can handle them differently.</p>
</li>
<li><p><code>sports/football/events</code>: A topic for match events such as goals and cards. Subscribers receive <code>{ type: 'match_event', matchId, event }</code>.</p>
</li>
</ul>
<p>The <code>#</code> wildcard in a subscription means "match this level and all levels below." So <code>sports/football/#</code> subscribes to every topic under <code>sports/football</code>, including <code>sports/football/match/abc123</code> and <code>sports/football/scores</code>. The <code>+</code> wildcard matches exactly one level. For example, <code>sports/+/match/#</code> would match any sport, not just football.</p>
<h3 id="heading-why-server-sent-events-instead-of-websockets">Why Server-Sent Events Instead of WebSockets?</h3>
<p>You might wonder why this tutorial uses Server-Sent Events (SSE) instead of WebSockets. Both can push data to the browser. The main difference:</p>
<ul>
<li><p><strong>Server-Sent Events</strong>: One-way (server to client). Built on HTTP. Automatic reconnection in the browser. Simpler to implement. No extra libraries.</p>
</li>
<li><p><strong>WebSockets</strong>: Two-way. Requires a different protocol. More flexible but more complex.</p>
</li>
</ul>
<p>For a sports score viewer, you only need server-to-client updates. The viewer never sends messages back over the same channel. Server-Sent Events is a better fit. If you later need the client to send commands (for example, to filter by league), you can add a separate HTTP API or switch to WebSockets.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>Start by creating a new folder for your project and initialize it with npm. The <code>mkdir</code> command creates the directory, <code>cd</code> moves into it, and <code>npm init -y</code> creates a <code>package.json</code> file with default values without prompting you for input.</p>
<pre><code class="language-shell">mkdir mqtt-football-scores
cd mqtt-football-scores
npm init -y
</code></pre>
<p>Install the required dependencies. Each package serves a specific role in the application. Run this command in the project root directory.</p>
<pre><code class="language-shell">npm install express cors mqtt uuid
</code></pre>
<ul>
<li><p><code>express</code>: Web framework for the HTTP server and API. It provides routing, middleware, and static file serving.</p>
</li>
<li><p><code>cors</code>: Enables Cross-Origin Resource Sharing so your frontend can call the API from a different origin (for example, if you serve the HTML from a different port or domain).</p>
</li>
<li><p><code>mqtt</code>: MQTT client for Node.js. It handles connection, publish, subscribe, reconnection, and Quality of Service (QoS) flows.</p>
</li>
<li><p><code>uuid</code>: Generates unique identifiers (Universally Unique Identifiers) for matches and events. Each ID is practically unique across all systems.</p>
</li>
</ul>
<p>Next, create the following folder structure. The <code>server</code> folder holds the Node.js backend code. The <code>public</code> folder holds the HTML, CSS, and client-side JavaScript that the browser loads. The <code>routes</code> subfolder keeps the match-related route handlers separate from the main server file.</p>
<pre><code class="language-plaintext">mqtt-football-scores/
├── server/
│   ├── index.js
│   ├── sse.js
│   └── routes/
│       └── matches.js
├── public/
│   ├── index.html
│   ├── admin.html
│   └── viewer.html
├── mosquitto.conf
├── docker-compose.yml
└── package.json
</code></pre>
<p>Add <code>"type": "module"</code> to your <code>package.json</code> so you can use JavaScript modules (import and export). The <code>type</code> field tells Node.js to treat <code>.js</code> files as ES modules, which allows you to use <code>import</code> and <code>export</code> syntax instead of CommonJS <code>require</code> and <code>module.exports</code>.</p>
<pre><code class="language-json">{
  "name": "mqtt-football-scores",
  "version": "1.0.0",
  "type": "module",
  "main": "server/index.js"
}
</code></pre>
<h2 id="heading-how-to-set-up-the-mqtt-broker">How to Set Up the MQTT Broker</h2>
<p>You need an MQTT broker running before the server can connect. You have three options.</p>
<h3 id="heading-option-1-docker-recommended">Option 1: Docker (Recommended)</h3>
<p>Create a <code>docker-compose.yml</code> file. This file defines a single service named <code>mosquitto</code> that runs the Eclipse Mosquitto 2 image. The <code>ports</code> directive maps port 1883 on your host to port 1883 in the container so your Express server can connect. The <code>volumes</code> directive mounts your local <code>mosquitto.conf</code> into the container so the broker uses your configuration. The <code>restart: unless-stopped</code> option ensures the container restarts automatically if it crashes or if you reboot your machine.</p>
<pre><code class="language-yaml">version: "3.8"

services:
  mosquitto:
    image: eclipse-mosquitto:2
    container_name: mqtt-football-mosquitto
    ports:
      - "1883:1883"
    volumes:
      - ./mosquitto.conf:/mosquitto/config/mosquitto.conf
    restart: unless-stopped
</code></pre>
<p>Create a <code>mosquitto.conf</code> file. The <code>listener 1883</code> directive tells Mosquitto to listen on port 1883 for MQTT connections. The <code>protocol mqtt</code> specifies the standard MQTT protocol (as opposed to WebSocket). The <code>allow_anonymous true</code> setting permits connections without a username and password, which is fine for local development but should be disabled in production. The <code>log_dest stdout</code> and <code>log_type all</code> directives send all log output to the console so you can debug connection issues.</p>
<pre><code class="language-plaintext">listener 1883
protocol mqtt
allow_anonymous true
log_dest stdout
log_type all
</code></pre>
<p>Start the broker:</p>
<pre><code class="language-bash">docker-compose up -d
</code></pre>
<h3 id="heading-option-2-local-install">Option 2: Local Install</h3>
<p>On macOS with Homebrew:</p>
<pre><code class="language-bash">brew install mosquitto
mosquitto -c mosquitto.conf
</code></pre>
<p>On Ubuntu or Debian:</p>
<pre><code class="language-bash">sudo apt install mosquitto mosquitto-clients
sudo systemctl start mosquitto
</code></pre>
<h3 id="heading-option-3-public-test-broker">Option 3: Public Test Broker</h3>
<p>You can use the public test broker at <code>test.mosquitto.org</code> without installing anything. Set the environment variable when you start the server:</p>
<pre><code class="language-bash">MQTT_BROKER=mqtt://test.mosquitto.org npm start
</code></pre>
<p>Note: The public broker is shared and not suitable for production. Use it only for development and testing.</p>
<h2 id="heading-how-to-build-the-express-server">How to Build the Express Server</h2>
<p>In this step, you'll create the main server that powers the real-time application. The Express server handles HTTP requests, serves static files like HTML and JavaScript, and acts as the bridge between the browser and the MQTT broker. It also provides endpoints that allow data to be sent and received in real time. Essentially, this server is the backbone of the application, enabling communication between all components.</p>
<p>Begin by creating the main server file at <code>server/index.js</code>:</p>
<pre><code class="language-typescript">import express from 'express';
import cors from 'cors';
import mqtt from 'mqtt';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import { v4 as uuidv4 } from 'uuid';

import { matchRoutes } from './routes/matches.js';
import { setupSSE, addSSEClient } from './sse.js';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const MQTT_BROKER = process.env.MQTT_BROKER || 'mqtt://localhost:1883';
const PORT = process.env.PORT || 3000;

const app = express();
app.use(cors());
app.use(express.json());
app.use(express.static(join(__dirname, '../public')));

let mqttClient = null;

function connectMQTT() {
  mqttClient = mqtt.connect(MQTT_BROKER, {
    clientId: `football-scores-${uuidv4().slice(0, 8)}`,
    reconnectPeriod: 3000,
    connectTimeout: 10000,
  });

  mqttClient.on('connect', () =&gt; {
    console.log('Connected to MQTT broker at', MQTT_BROKER);
    mqttClient.subscribe('sports/football/#', { qos: 1 }, (err) =&gt; {
      if (err) console.error('Subscribe error:', err);
    });
  });

  mqttClient.on('error', (err) =&gt; {
    console.error('MQTT error:', err.message);
  });

  mqttClient.on('close', () =&gt; {
    console.log('MQTT connection closed');
  });

  mqttClient.on('reconnect', () =&gt; {
    console.log('MQTT reconnecting...');
  });

  return mqttClient;
}

const mqttClientInstance = connectMQTT();
const { publishMatch, publishScoreUpdate, publishEvent, getMatches } = matchRoutes(mqttClientInstance);
setupSSE(mqttClientInstance);

app.get('/api/events', (req, res) =&gt; addSSEClient(res));
app.post('/api/matches', publishMatch);
app.patch('/api/matches/:id/score', publishScoreUpdate);
app.post('/api/matches/:id/events', publishEvent);
app.get('/api/matches', getMatches);

app.listen(PORT, () =&gt; {
  console.log(`Football Scores Server running at http://localhost:${PORT}`);
  console.log(`Admin (upload):  http://localhost:${PORT}/admin.html`);
  console.log(`Viewer:          http://localhost:${PORT}/viewer.html`);
});
</code></pre>
<p>Here is what each part does:</p>
<ul>
<li><p><strong>Imports</strong>: The <code>fileURLToPath</code> and <code>dirname</code> utilities replicate the <code>__dirname</code> variable that CommonJS provides, since ES modules don't have it. You need <code>__dirname</code> to build the path to the <code>public</code> folder.</p>
</li>
<li><p><strong>Environment variables</strong>: <code>MQTT_BROKER</code> defaults to <code>mqtt://localhost:1883</code> so the server connects to a local Mosquitto instance. You can override it for Docker or a remote broker. <code>PORT</code> defaults to 3000.</p>
</li>
<li><p><strong>Middleware</strong>: <code>cors()</code> allows requests from any origin, which is useful during development. <code>express.json()</code> parses JSON request bodies. <code>express.static()</code> serves files from <code>public</code> so <code>/admin.html</code> and <code>/viewer.html</code> are available.</p>
</li>
<li><p><strong>connectMQTT</strong>: Creates an MQTT client with a unique client ID (required by the broker), a 3-second reconnect interval, and a 10-second connection timeout. On connect, it subscribes to <code>sports/football/#</code> with QoS 1. The <code>#</code> wildcard means "all topics under sports/football."</p>
</li>
<li><p><strong>matchRoutes</strong>: Returns the route handlers that create matches, update scores, and add events. Each handler publishes to MQTT and responds with JSON.</p>
</li>
<li><p><strong>setupSSE</strong>: Registers a listener on the MQTT client's <code>message</code> event. When a message arrives, it forwards the payload to all connected Server-Sent Events clients.</p>
</li>
<li><p><strong>addSSEClient</strong>: Called when a viewer opens <code>/api/events</code>. It sets the response headers for Server-Sent Events, flushes the headers so the connection stays open, and adds the response object to a set of active clients.</p>
</li>
<li><p><strong>Routes</strong>: The GET <code>/api/events</code> route establishes the Server-Sent Events stream. The POST, PATCH, and GET routes for matches delegate to the handlers from <code>matchRoutes</code>.</p>
</li>
</ul>
<p>The server serves static files from the <code>public</code> folder, so your HTML pages are available at the root.</p>
<h2 id="heading-how-to-implement-the-match-routes">How to Implement the Match Routes</h2>
<p>In this step, you'll create the route handlers that manage matches, scores, and events. These routes allow the server to receive requests from the admin interface, update match data, and publish real-time updates to MQTT. They also store match information in memory so it can be retrieved and updated during the session.</p>
<p>Begin by creating the file at <code>server/routes/matches.js</code>:</p>
<pre><code class="language-javascript">import { v4 as uuidv4 } from 'uuid';

const TOPIC_MATCH = 'sports/football/match';
const TOPIC_SCORES = 'sports/football/scores';
const TOPIC_EVENTS = 'sports/football/events';

const matches = new Map();

function publish(client, topic, payload, qos = 1) {
  if (!client?.connected) {
    console.warn('MQTT not connected, message not published');
    return false;
  }
  client.publish(topic, JSON.stringify(payload), { qos, retain: false });
  return true;
}

export function matchRoutes(mqttClient) {
  return {
    publishMatch: (req, res) =&gt; {
      const { homeTeam, awayTeam, league, venue, kickoff } = req.body;
      if (!homeTeam || !awayTeam) {
        return res.status(400).json({ error: 'homeTeam and awayTeam are required' });
      }

      const match = {
        id: uuidv4(),
        homeTeam,
        awayTeam,
        homeScore: 0,
        awayScore: 0,
        league: league || 'Premier League',
        venue: venue || 'TBD',
        kickoff: kickoff || new Date().toISOString(),
        status: 'scheduled',
        minute: 0,
        events: [],
        createdAt: new Date().toISOString(),
      };

      matches.set(match.id, match);

      const topic = `\({TOPIC_MATCH}/\){match.id}`;
      publish(mqttClient, topic, match);
      publish(mqttClient, TOPIC_SCORES, { type: 'match_created', match });

      res.status(201).json(match);
    },

    publishScoreUpdate: (req, res) =&gt; {
      const { id } = req.params;
      const { homeScore, awayScore, minute, status } = req.body;

      const match = matches.get(id);
      if (!match) {
        return res.status(404).json({ error: 'Match not found' });
      }

      if (homeScore !== undefined) match.homeScore = homeScore;
      if (awayScore !== undefined) match.awayScore = awayScore;
      if (minute !== undefined) match.minute = minute;
      if (status !== undefined) match.status = status;

      const topic = `\({TOPIC_MATCH}/\){id}`;
      publish(mqttClient, topic, match);
      publish(mqttClient, TOPIC_SCORES, {
        type: 'score_update',
        matchId: id,
        homeScore: match.homeScore,
        awayScore: match.awayScore,
        minute: match.minute,
        status: match.status,
      });

      res.json(match);
    },

    publishEvent: (req, res) =&gt; {
      const { id } = req.params;
      const { type, team, player, minute, description } = req.body;

      const match = matches.get(id);
      if (!match) {
        return res.status(404).json({ error: 'Match not found' });
      }

      const event = {
        id: uuidv4().slice(0, 8),
        type: type || 'goal',
        team,
        player: player || 'Unknown',
        minute: minute ?? match.minute,
        description: description || `\({type}: \){player}`,
        timestamp: new Date().toISOString(),
      };

      match.events.push(event);
      if (type === 'goal') {
        if (team === match.homeTeam) match.homeScore++;
        else if (team === match.awayTeam) match.awayScore++;
      }

      const topic = `\({TOPIC_MATCH}/\){id}`;
      publish(mqttClient, topic, match);
      publish(mqttClient, TOPIC_EVENTS, { type: 'match_event', matchId: id, event });

      res.status(201).json({ match, event });
    },

    getMatches: (req, res) =&gt; {
      const list = Array.from(matches.values()).sort(
        (a, b) =&gt; new Date(b.createdAt) - new Date(a.createdAt)
      );
      res.json(list);
    },
  };
}
</code></pre>
<p>The routes use an in-memory <code>Map</code> to store matches. In production, you would replace this with a database such as PostgreSQL or MongoDB.</p>
<p>Explanation of the key logic:</p>
<ul>
<li><p><strong>publish</strong>: A helper that checks if the MQTT client is connected before publishing. If the broker is down, it logs a warning instead of throwing. The <code>retain: false</code> option means the broker doesn't store the last message for new subscribers.</p>
</li>
<li><p><strong>publishMatch</strong>: Validates that <code>homeTeam</code> and <code>awayTeam</code> are present. Creates a match object with default values for league, venue, kickoff, status, and events. Stores it in the Map, publishes to the match-specific topic and the scores topic, and returns the match with status 201 (Created).</p>
</li>
<li><p><strong>publishScoreUpdate</strong>: Looks up the match by ID. If not found, returns 404. Updates only the fields that are provided (using <code>!== undefined</code> so you can set a score to 0). Publishes the full match and a score_update notification.</p>
</li>
<li><p><strong>publishEvent</strong>: Creates an event object with a short unique ID. Pushes it to the match's events array. If the event type is <code>goal</code>, increments the home or away score based on the team. Publishes the updated match and an event notification.</p>
</li>
<li><p><strong>getMatches</strong>: Converts the Map to an array, sorts by <code>createdAt</code> descending (newest first), and returns the list as JSON.</p>
</li>
</ul>
<p>MQTT topics used:</p>
<ul>
<li><p><code>sports/football/match/{id}</code>: Full match state. Used when a match is created or updated.</p>
</li>
<li><p><code>sports/football/scores</code>: Score change notifications. Used for match creation and score updates.</p>
</li>
<li><p><code>sports/football/events</code>: Match events such as goals and cards.</p>
</li>
</ul>
<p>The <code>publish</code> function sends JavaScript Object Notation (JSON) payloads with QoS 1 (at least once delivery). MQTT defines three QoS levels: 0 (at most once), 1 (at least once), and 2 (exactly once). Level 1 ensures the broker will retry until the subscriber acknowledges the message, which reduces the chance of losing score updates if the connection drops briefly.</p>
<h2 id="heading-how-to-bridge-mqtt-to-the-browser-with-server-sent-events">How to Bridge MQTT to the Browser with Server-Sent Events</h2>
<p>In this step, you'll create the Server-Sent Events (SSE) module that bridges MQTT messages to the browser. MQTT works over TCP and browsers cannot connect to it directly, so we use SSE as a lightweight HTTP-based streaming channel. SSE keeps an open connection and allows the server to push updates to connected browsers in real time. This is what enables viewers to see match updates instantly without refreshing.</p>
<p>Now create the file at <code>server/sse.js</code>:</p>
<pre><code class="language-javascript">const clients = new Set();

export function setupSSE(mqttClient) {
  if (!mqttClient) return;

  mqttClient.on('message', (topic, message) =&gt; {
    try {
      const payload = JSON.parse(message.toString());
      const data = JSON.stringify({ topic, ...payload });
      clients.forEach((res) =&gt; {
        try {
          res.write(`data: ${data}\n\n`);
        } catch (e) {
          clients.delete(res);
        }
      });
    } catch (e) {
      console.error('SSE parse error:', e.message);
    }
  });
}

export function addSSEClient(res) {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.setHeader('X-Accel-Buffering', 'no');
  res.flushHeaders();

  clients.add(res);

  res.on('close', () =&gt; {
    clients.delete(res);
  });
}
</code></pre>
<p>Explanation of each part:</p>
<ul>
<li><p><strong>clients Set</strong>: A Set holds all active Server-Sent Events response objects. Using a Set makes it easy to add and remove clients without duplicates.</p>
</li>
<li><p><strong>setupSSE</strong>: Attaches a <code>message</code> listener to the MQTT client. When any message arrives on a subscribed topic, the callback runs. It parses the message payload (which is JSON), merges the topic into the payload with <code>{ topic, ...payload }</code>, and sends the result to every client. The Server-Sent Events format requires each message to be <code>data: {content}\n\n</code> (two newlines). The <code>forEach</code> loop catches write errors (for example, if a client disconnected) and removes the client from the set.</p>
</li>
<li><p><strong>addSSEClient</strong>: Sets the <code>Content-Type</code> header to <code>text/event-stream</code> so the browser treats the response as an event stream. The <code>Cache-Control: no-cache</code> and <code>Connection: keep-alive</code> headers prevent the browser or proxy from caching or closing the connection. The <code>X-Accel-Buffering: no</code> header disables buffering in Nginx, which can delay or block Server-Sent Events. The <code>flushHeaders</code> call sends the headers immediately so the connection is established. The <code>close</code> event handler removes the client when the client disconnects (closes the tab or navigates away).</p>
</li>
</ul>
<p>Server-Sent Events is one-way (server to client). For this use case, that is enough because viewers only need to receive updates, not send messages back over the same channel.</p>
<h2 id="heading-how-to-build-the-admin-upload-interface">How to Build the Admin Upload Interface</h2>
<p>The admin interface is a single HTML page where match creators can create new matches, update scores, and add events such as goals or cards. It uses standard HTML forms so data can be submitted to the server, and JavaScript will later handle form submissions and dynamic updates. All the markup, styles, and script live in <code>public/admin.html</code>, which the Express server serves as a static page.</p>
<h3 id="heading-admin-html-structure-and-styles">Admin HTML Structure and Styles</h3>
<p>The document starts with the standard HTML5 boilerplate. The <code>charset</code> and <code>viewport</code> meta tags ensure proper character encoding and responsive layout on mobile devices. The page loads the Outfit font from Google Fonts for a clean, modern look.</p>
<p>The Cascading Style Sheets (CSS) uses custom properties (variables) in the <code>:root</code> block so you can change the color scheme in one place. The <code>--bg</code> variable holds the dark background color, <code>--surface</code> for card backgrounds, <code>--accent</code> for the green accent color, and <code>--text-muted</code> for secondary text. The <code>.grid</code> class creates a two-column layout for form fields that collapses to one column on screens under 600 pixels wide. The <code>.toast</code> class positions the notification at the bottom-right and uses <code>transform</code> and <code>opacity</code> for a slide-in animation when the <code>.show</code> class is added.</p>
<p>Now create the file at <code>public/admin.html</code>:</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Admin - Football Scores Upload&lt;/title&gt;
  &lt;link rel="preconnect" href="https://fonts.googleapis.com"&gt;
  &lt;link rel="preconnect" href="https://fonts.gstatic.com" crossorigin&gt;
  &lt;link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&amp;display=swap" rel="stylesheet"&gt;
  &lt;style&gt;
    :root {
      --bg: #0f1419;
      --surface: #1a2332;
      --surface-hover: #243044;
      --accent: #00d26a;
      --accent-dim: #00a854;
      --text: #e8edf2;
      --text-muted: #8b9aab;
      --border: #2d3a4d;
      --danger: #ff4757;
    }
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: 'Outfit', sans-serif;
      background: var(--bg);
      color: var(--text);
      min-height: 100vh;
      padding: 2rem;
    }
    .container { max-width: 900px; margin: 0 auto; }
    header {
      display: flex;
      align-items: center;
      gap: 1rem;
      margin-bottom: 2rem;
      padding-bottom: 1rem;
      border-bottom: 1px solid var(--border);
    }
    section {
      background: var(--surface);
      border-radius: 12px;
      padding: 1.5rem;
      margin-bottom: 1.5rem;
      border: 1px solid var(--border);
    }
    .grid {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 1rem;
    }
    @media (max-width: 600px) { .grid { grid-template-columns: 1fr; } }
    .form-group { display: flex; flex-direction: column; gap: 0.4rem; }
    .form-group.full { grid-column: 1 / -1; }
    input, select {
      padding: 0.65rem 1rem;
      border: 1px solid var(--border);
      border-radius: 8px;
      background: var(--bg);
      color: var(--text);
      font-family: inherit;
    }
    button {
      padding: 0.75rem 1.5rem;
      border: none;
      border-radius: 8px;
      font-family: inherit;
      font-weight: 600;
      cursor: pointer;
    }
    .btn-primary { background: var(--accent); color: var(--bg); }
    .btn-secondary { background: var(--surface-hover); color: var(--text); border: 1px solid var(--border); }
    .actions { display: flex; gap: 0.75rem; flex-wrap: wrap; margin-top: 1rem; }
    .badge { background: var(--accent); color: var(--bg); font-size: 0.75rem; padding: 0.25rem 0.6rem; border-radius: 999px; font-weight: 600; }
    .viewer-link { margin-left: auto; color: var(--accent); text-decoration: none; font-weight: 500; }
    section h2 { font-size: 1rem; font-weight: 600; margin-bottom: 1rem; color: var(--text-muted); }
    label { font-size: 0.85rem; font-weight: 500; color: var(--text-muted); }
    .toast {
      position: fixed;
      bottom: 2rem;
      right: 2rem;
      padding: 1rem 1.5rem;
      border-radius: 8px;
      font-weight: 500;
      color: var(--bg);
      background: var(--accent);
      transform: translateY(100px);
      opacity: 0;
      transition: transform 0.3s, opacity 0.3s;
      z-index: 100;
    }
    .toast.show { transform: translateY(0); opacity: 1; }
    .toast.error { background: var(--danger); }
    .match-card {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 1rem;
      background: var(--bg);
      border-radius: 8px;
      margin-bottom: 0.5rem;
      border: 1px solid var(--border);
    }
    .match-score { font-size: 1.5rem; font-weight: 700; color: var(--accent); margin: 0 1rem; }
    .match-info { flex: 1; }
    .match-teams { font-weight: 600; font-size: 1rem; }
    .match-meta { font-size: 0.8rem; color: var(--text-muted); margin-top: 0.25rem; }
    .match-actions { display: flex; gap: 0.5rem; }
    .match-actions button { padding: 0.5rem 1rem; font-size: 0.85rem; }
    .match-list { margin-top: 1rem; }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;div class="container"&gt;
    &lt;header&gt;
      &lt;h1&gt;⚽ Football Scores&lt;/h1&gt;
      &lt;span class="badge"&gt;Admin&lt;/span&gt;
      &lt;a href="/viewer.html" class="viewer-link"&gt;→ Open Viewer&lt;/a&gt;
    &lt;/header&gt;

    &lt;section&gt;
      &lt;h2&gt;Create New Match&lt;/h2&gt;
      &lt;form id="createMatch"&gt;
        &lt;div class="grid"&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="homeTeam"&gt;Home Team&lt;/label&gt;
            &lt;input type="text" id="homeTeam" placeholder="e.g. Manchester United" required&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="awayTeam"&gt;Away Team&lt;/label&gt;
            &lt;input type="text" id="awayTeam" placeholder="e.g. Liverpool" required&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="league"&gt;League&lt;/label&gt;
            &lt;input type="text" id="league" placeholder="e.g. Premier League" value="Premier League"&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="venue"&gt;Venue&lt;/label&gt;
            &lt;input type="text" id="venue" placeholder="e.g. Old Trafford"&gt;
          &lt;/div&gt;
          &lt;div class="form-group full"&gt;
            &lt;label for="kickoff"&gt;Kickoff (ISO)&lt;/label&gt;
            &lt;input type="datetime-local" id="kickoff"&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="actions"&gt;
          &lt;button type="submit" class="btn-primary"&gt;Create Match&lt;/button&gt;
        &lt;/div&gt;
      &lt;/form&gt;
    &lt;/section&gt;

    &lt;section&gt;
      &lt;h2&gt;Update Score&lt;/h2&gt;
      &lt;form id="updateScore"&gt;
        &lt;div class="grid"&gt;
          &lt;div class="form-group full"&gt;
            &lt;label for="scoreMatchId"&gt;Select Match&lt;/label&gt;
            &lt;select id="scoreMatchId" required&gt;
              &lt;option value=""&gt;-- Select match --&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="homeScore"&gt;Home Score&lt;/label&gt;
            &lt;input type="number" id="homeScore" min="0" value="0"&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="awayScore"&gt;Away Score&lt;/label&gt;
            &lt;input type="number" id="awayScore" min="0" value="0"&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="minute"&gt;Minute&lt;/label&gt;
            &lt;input type="number" id="minute" min="0" placeholder="e.g. 67"&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="status"&gt;Status&lt;/label&gt;
            &lt;select id="status"&gt;
              &lt;option value="scheduled"&gt;Scheduled&lt;/option&gt;
              &lt;option value="live"&gt;Live&lt;/option&gt;
              &lt;option value="halftime"&gt;Halftime&lt;/option&gt;
              &lt;option value="finished"&gt;Finished&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="actions"&gt;
          &lt;button type="submit" class="btn-primary"&gt;Update Score&lt;/button&gt;
        &lt;/div&gt;
      &lt;/form&gt;
    &lt;/section&gt;

    &lt;section&gt;
      &lt;h2&gt;Add Match Event (Goal, Card, etc.)&lt;/h2&gt;
      &lt;form id="addEvent"&gt;
        &lt;div class="grid"&gt;
          &lt;div class="form-group full"&gt;
            &lt;label for="eventMatchId"&gt;Select Match&lt;/label&gt;
            &lt;select id="eventMatchId" required&gt;
              &lt;option value=""&gt;-- Select match --&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="eventType"&gt;Event Type&lt;/label&gt;
            &lt;select id="eventType"&gt;
              &lt;option value="goal"&gt;Goal&lt;/option&gt;
              &lt;option value="yellow_card"&gt;Yellow Card&lt;/option&gt;
              &lt;option value="red_card"&gt;Red Card&lt;/option&gt;
              &lt;option value="substitution"&gt;Substitution&lt;/option&gt;
              &lt;option value="penalty"&gt;Penalty&lt;/option&gt;
            &lt;/select&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="eventTeam"&gt;Team&lt;/label&gt;
            &lt;input type="text" id="eventTeam" placeholder="e.g. Manchester United"&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="eventPlayer"&gt;Player&lt;/label&gt;
            &lt;input type="text" id="eventPlayer" placeholder="e.g. Marcus Rashford"&gt;
          &lt;/div&gt;
          &lt;div class="form-group"&gt;
            &lt;label for="eventMinute"&gt;Minute&lt;/label&gt;
            &lt;input type="number" id="eventMinute" min="0" placeholder="e.g. 23"&gt;
          &lt;/div&gt;
          &lt;div class="form-group full"&gt;
            &lt;label for="eventDesc"&gt;Description&lt;/label&gt;
            &lt;input type="text" id="eventDesc" placeholder="Optional description"&gt;
          &lt;/div&gt;
        &lt;/div&gt;
        &lt;div class="actions"&gt;
          &lt;button type="submit" class="btn-primary"&gt;Add Event&lt;/button&gt;
        &lt;/div&gt;
      &lt;/form&gt;
    &lt;/section&gt;

    &lt;section&gt;
      &lt;h2&gt;Active Matches&lt;/h2&gt;
      &lt;div class="match-list" id="matchList"&gt;&lt;/div&gt;
    &lt;/section&gt;
  &lt;/div&gt;

  &lt;div class="toast" id="toast"&gt;&lt;/div&gt;
</code></pre>
<p>The three forms use <code>id</code> attributes (<code>createMatch</code>, <code>updateScore</code>, <code>addEvent</code>) so the JavaScript can attach submit handlers. The match dropdowns (<code>scoreMatchId</code> and <code>eventMatchId</code>) are populated dynamically when the page loads. The <code>matchList</code> div is the container for the list of active matches. The toast element sits outside the main container so it can be fixed to the viewport.</p>
<h3 id="heading-admin-javascript-logic">Admin JavaScript Logic</h3>
<p>The script block handles all interaction between the admin page and the server. It defines helper functions for showing notifications, fetching matches, updating the UI, and submitting data. When the page loads or after any action (create, update, or event submission), the UI refreshes so the latest match data is always visible.</p>
<pre><code class="language-javascript">  &lt;script&gt;
    const API = '/api';
    const toast = document.getElementById('toast');

    function showToast(msg, isError = false) {
      toast.textContent = msg;
      toast.className = 'toast show' + (isError ? ' error' : '');
      setTimeout(() =&gt; toast.classList.remove('show'), 3000);
    }

    async function fetchMatches() {
      const res = await fetch(`${API}/matches`);
      return res.json();
    }

    function populateSelects(matches) {
      const opts = matches.map(m =&gt; `&lt;option value="\({m.id}"&gt;\){m.homeTeam} vs ${m.awayTeam}&lt;/option&gt;`).join('');
      const html = '&lt;option value=""&gt;-- Select match --&lt;/option&gt;' + opts;
      document.getElementById('scoreMatchId').innerHTML = html;
      document.getElementById('eventMatchId').innerHTML = html;
    }

    function renderMatchList(matches) {
      const list = document.getElementById('matchList');
      if (!matches.length) {
        list.innerHTML = '&lt;p style="color: var(--text-muted);"&gt;No matches yet. Create one above.&lt;/p&gt;';
        return;
      }
      list.innerHTML = matches.map(m =&gt; `
        &lt;div class="match-card" data-id="${m.id}"&gt;
          &lt;div class="match-info"&gt;
            &lt;div class="match-teams"&gt;\({m.homeTeam} vs \){m.awayTeam}&lt;/div&gt;
            &lt;div class="match-meta"&gt;\({m.league} • \){m.status} • ${m.minute}'&lt;/div&gt;
          &lt;/div&gt;
          &lt;div class="match-score"&gt;\({m.homeScore} - \){m.awayScore}&lt;/div&gt;
          &lt;div class="match-actions"&gt;
            &lt;button class="btn-secondary" onclick="quickScore('\({m.id}', \){m.homeScore}, ${m.awayScore})"&gt;Update&lt;/button&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      `).join('');
    }

    function quickScore(id, h, a) {
      document.getElementById('scoreMatchId').value = id;
      document.getElementById('homeScore').value = h;
      document.getElementById('awayScore').value = a;
    }

    async function loadMatches() {
      const matches = await fetchMatches();
      populateSelects(matches);
      renderMatchList(matches);
    }

    document.getElementById('createMatch').onsubmit = async (e) =&gt; {
      e.preventDefault();
      const body = {
        homeTeam: document.getElementById('homeTeam').value.trim(),
        awayTeam: document.getElementById('awayTeam').value.trim(),
        league: document.getElementById('league').value.trim() || 'Premier League',
        venue: document.getElementById('venue').value.trim() || 'TBD',
        kickoff: document.getElementById('kickoff').value
          ? new Date(document.getElementById('kickoff').value).toISOString()
          : new Date().toISOString(),
      };
      try {
        const res = await fetch(`${API}/matches`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(body),
        });
        const data = await res.json();
        if (res.ok) {
          showToast('Match created!');
          document.getElementById('createMatch').reset();
          loadMatches();
        } else showToast(data.error || 'Failed', true);
      } catch (err) {
        showToast('Network error', true);
      }
    };

    document.getElementById('updateScore').onsubmit = async (e) =&gt; {
      e.preventDefault();
      const id = document.getElementById('scoreMatchId').value;
      const body = {
        homeScore: parseInt(document.getElementById('homeScore').value, 10),
        awayScore: parseInt(document.getElementById('awayScore').value, 10),
        minute: parseInt(document.getElementById('minute').value, 10) || undefined,
        status: document.getElementById('status').value,
      };
      try {
        const res = await fetch(`\({API}/matches/\){id}/score`, {
          method: 'PATCH',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(body),
        });
        const data = await res.json();
        if (res.ok) {
          showToast('Score updated!');
          loadMatches();
        } else showToast(data.error || 'Failed', true);
      } catch (err) {
        showToast('Network error', true);
      }
    };

    document.getElementById('addEvent').onsubmit = async (e) =&gt; {
      e.preventDefault();
      const id = document.getElementById('eventMatchId').value;
      const body = {
        type: document.getElementById('eventType').value,
        team: document.getElementById('eventTeam').value.trim(),
        player: document.getElementById('eventPlayer').value.trim(),
        minute: parseInt(document.getElementById('eventMinute').value, 10) || undefined,
        description: document.getElementById('eventDesc').value.trim() || undefined,
      };
      try {
        const res = await fetch(`\({API}/matches/\){id}/events`, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(body),
        });
        const data = await res.json();
        if (res.ok) {
          showToast('Event added!');
          loadMatches();
        } else showToast(data.error || 'Failed', true);
      } catch (err) {
        showToast('Network error', true);
      }
    };

    loadMatches();
  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>Explanation of each part:</p>
<ul>
<li><p><code>showToast</code>: updates the toast text and adds the show class to trigger the CSS animation. The isError parameter switches the toast to a red background for error messages. The setTimeout removes the show class after 3 seconds so the toast slides back out.</p>
</li>
<li><p><code>fetchMatches</code>: calls the GET /api/matches endpoint and returns the parsed JSON so the UI can display the latest data.</p>
</li>
<li><p><code>populateSelects</code>: builds option elements from the matches array and injects them into both dropdowns, so the same match list appears in the Update Score and Add Event forms.</p>
</li>
<li><p><code>renderMatchList</code>: either shows a placeholder when there are no matches or renders each match as a card with team names, scores, league, status, and an Update button.</p>
</li>
<li><p><code>quickScore</code>: pre-fills the Update Score form when you click the Update button on a match card, so you can adjust the score without re-selecting the match.</p>
</li>
<li><p><code>loadMatches</code>: fetches matches, populates the dropdowns, and renders the list. It runs on page load and after every successful create, update, or event submission.</p>
</li>
<li><p><code>onsubmit</code>: calls e.preventDefault() to stop the default form submission, builds a request body from the form values, sends a fetch request to the appropriate endpoint, and on success shows a toast and calls loadMatches to refresh the UI.</p>
</li>
</ul>
<h2 id="heading-how-to-build-the-live-viewer-interface">How to Build the Live Viewer Interface</h2>
<p>The viewer interface displays real-time match updates and connects to the Server-Sent Events endpoint so it can receive data the moment the server pushes it.</p>
<p>Unlike the admin page, the viewer is read-only: it shows live scores, match events, and status updates without requiring user input. The page uses a dark theme and a connection indicator so users can see whether the real-time stream is active.</p>
<p>Now create the file at <code>public/viewer.html</code>:</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Live Football Scores&lt;/title&gt;
  &lt;link rel="preconnect" href="https://fonts.googleapis.com"&gt;
  &lt;link rel="preconnect" href="https://fonts.gstatic.com" crossorigin&gt;
  &lt;link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&amp;display=swap" rel="stylesheet"&gt;
  &lt;style&gt;
    :root {
      --bg: #0a0e14;
      --surface: #131a24;
      --accent: #00d26a;
      --accent-glow: rgba(0, 210, 106, 0.3);
      --text: #e8edf2;
      --text-muted: #8b9aab;
      --border: #2d3a4d;
      --live: #ff4757;
    }
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: 'Outfit', sans-serif;
      background: var(--bg);
      color: var(--text);
      min-height: 100vh;
      padding: 2rem;
    }
    .container { max-width: 700px; margin: 0 auto; }
    header { text-align: center; margin-bottom: 2rem; }
    .status {
      display: inline-flex;
      align-items: center;
      gap: 0.5rem;
      font-size: 0.85rem;
      color: var(--text-muted);
    }
    .status-dot {
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--text-muted);
      animation: pulse 2s infinite;
    }
    .status-dot.connected {
      background: var(--accent);
      box-shadow: 0 0 0 3px var(--accent-glow);
    }
    @keyframes pulse {
      0%, 100% { opacity: 1; }
      50% { opacity: 0.5; }
    }
    .match-card {
      background: var(--surface);
      border-radius: 16px;
      padding: 1.5rem;
      margin-bottom: 1rem;
      border: 1px solid var(--border);
      transition: border-color 0.2s, box-shadow 0.2s;
    }
    .match-card.live {
      border-color: var(--live);
      box-shadow: 0 0 0 1px rgba(255, 71, 87, 0.2);
    }
    .match-header {
      display: flex;
      align-items: center;
      justify-content: space-between;
      margin-bottom: 1rem;
    }
    .league { 
      font-size: 0.8rem; 
      color: var(--text-muted); 
      margin-bottom: 0.25rem; 
    }
    .match-teams {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 1rem;
      margin: 1rem 0;
    }
    .team { flex: 1; text-align: center; font-weight: 600; font-size: 1.1rem; }
    .team.home { text-align: left; }
    .team.away { text-align: right; }
    .score-box {
      display: flex;
      align-items: center;
      justify-content: center;
      min-width: 80px;
      gap: 0.5rem;
    }
    .score { 
      font-size: 2rem; 
      font-weight: 700; 
      color: var(--accent); 
     }
    .status-badge { 
      font-size: 0.7rem; 
      padding: 0.2rem 0.5rem; 
      border-radius: 4px; 
      font-weight: 600; 
     }
    .status-badge.live { background: var(--live); color: white; }
    .status-badge.finished { background: var(--border); color: var(--text-muted); }
    .status-badge.scheduled { background: var(--accent); color: var(--bg); }
    .match-meta { font-size: 0.85rem; color: var(--text-muted); margin-top: 0.5rem; }
    .events { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border); }
    .events h4 { font-size: 0.8rem; color: var(--text-muted); margin-bottom: 0.5rem; }
    .event { display: flex; align-items: center; gap: 0.5rem; font-size: 0.85rem; padding: 0.35rem 0; border-bottom: 1px solid var(--border); }
    .event:last-child { border-bottom: none; }
    .event-icon { width: 24px; text-align: center; font-size: 1rem; }
    .event.goal .event-icon { color: var(--accent); }
    .event.yellow_card .event-icon { color: #ffd93d; }
    .event.red_card .event-icon { color: var(--live); }
    .feed { margin-top: 2rem; padding-top: 1.5rem; border-top: 1px solid var(--border); }
    .feed h3 { font-size: 1rem; margin-bottom: 1rem; color: var(--text-muted); }
    .feed-item { font-size: 0.85rem; padding: 0.5rem 0; color: var(--text-muted); border-bottom: 1px solid var(--border); }
    .feed-item:last-child { border-bottom: none; }
    .feed-item strong { color: var(--text); }
    .empty { text-align: center; padding: 3rem 2rem; color: var(--text-muted); }
    .empty-icon { font-size: 3rem; margin-bottom: 1rem; opacity: 0.5; }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;div class="container"&gt;
    &lt;header&gt;
      &lt;h1&gt;⚽ Live Football Scores&lt;/h1&gt;
      &lt;div class="status" id="status"&gt;
        &lt;span class="status-dot" id="statusDot"&gt;&lt;/span&gt;
        &lt;span id="statusText"&gt;Connecting...&lt;/span&gt;
      &lt;/div&gt;
    &lt;/header&gt;

    &lt;div id="matches"&gt;&lt;/div&gt;

    &lt;div class="feed" id="feedSection"&gt;
      &lt;h3&gt;Live Feed&lt;/h3&gt;
      &lt;div id="feed"&gt;&lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
</code></pre>
<p>Explanation of each part:</p>
<ul>
<li><p><code>header</code>: displays the page title and a connection indicator so users know whether the real-time stream is active.</p>
</li>
<li><p><code>status dot</code>: a small circle that pulses while disconnected and turns green with a glow when the SSE connection is established.</p>
</li>
<li><p><code>matches container</code>: the div with id="matches" is where match cards will be rendered dynamically by JavaScript as data arrives.</p>
</li>
<li><p><code>feed section</code>: shows a chronological list of live updates so users can see recent events at a glance.</p>
</li>
<li><p><code>CSS theme</code>: uses dark colors and custom properties so the look can be adjusted in one place. The live badge and border styles highlight matches that are in progress.</p>
</li>
<li><p><code>Server-Sent Events integration</code>: JavaScript (added in the next step) will connect to /api/events and update this page whenever new data arrives.</p>
</li>
</ul>
<p>The header shows the title and a connection status indicator. The <code>matches</code> div is the container for match cards, populated by JavaScript. The <code>feed</code> div displays the live feed of recent updates.</p>
<h3 id="heading-viewer-javascript-logic">Viewer JavaScript Logic</h3>
<p>The script powers the live viewer interface by maintaining an in-memory collection of matches and a feed of recent updates. It connects to the Server-Sent Events endpoint so data flows from the server to the browser in real time. When messages arrive, the page updates automatically to show the latest scores, events, and match details.</p>
<pre><code class="language-javascript">  &lt;script&gt;
    const API = '/api';
    const matches = new Map();
    const feed = [];
    const MAX_FEED = 20;

    const statusDot = document.getElementById('statusDot');
    const statusText = document.getElementById('statusText');
    const matchesEl = document.getElementById('matches');
    const feedEl = document.getElementById('feed');

    function setStatus(connected) {
      statusDot.classList.toggle('connected', connected);
      statusText.textContent = connected ? 'Live' : 'Reconnecting...';
    }

    function renderMatches() {
      const list = Array.from(matches.values()).sort(
        (a, b) =&gt; new Date(b.createdAt) - new Date(a.createdAt)
      );
      if (!list.length) {
        matchesEl.innerHTML = `
          &lt;div class="empty"&gt;
            &lt;div class="empty-icon"&gt;⚽&lt;/div&gt;
            &lt;p&gt;No matches yet. Updates will appear here in real-time.&lt;/p&gt;
          &lt;/div&gt;
        `;
        return;
      }
      matchesEl.innerHTML = list.map(m =&gt; `
        &lt;div class="match-card \({m.status === 'live' ? 'live' : ''}" data-id="\){m.id}"&gt;
          &lt;div class="match-header"&gt;
            &lt;div&gt;
              &lt;div class="league"&gt;${m.league}&lt;/div&gt;
              &lt;div class="match-meta"&gt;\({m.venue} • \){m.kickoff ? new Date(m.kickoff).toLocaleString() : ''}&lt;/div&gt;
            &lt;/div&gt;
            &lt;span class="status-badge \({m.status}"&gt;\){m.status}&lt;/span&gt;
          &lt;/div&gt;
          &lt;div class="match-teams"&gt;
            &lt;div class="team home"&gt;${m.homeTeam}&lt;/div&gt;
            &lt;div class="score-box"&gt;
              &lt;span class="score"&gt;${m.homeScore}&lt;/span&gt;
              &lt;span&gt;-&lt;/span&gt;
              &lt;span class="score"&gt;${m.awayScore}&lt;/span&gt;
            &lt;/div&gt;
            &lt;div class="team away"&gt;${m.awayTeam}&lt;/div&gt;
          &lt;/div&gt;
          \({m.minute ? `&lt;div class="match-meta"&gt;\){m.minute}'&lt;/div&gt;` : ''}
          ${m.events?.length ? `
            &lt;div class="events"&gt;
              &lt;h4&gt;Events&lt;/h4&gt;
              ${m.events.map(e =&gt; `
                &lt;div class="event ${e.type}"&gt;
                  &lt;span class="event-icon"&gt;${getEventIcon(e.type)}&lt;/span&gt;
                  &lt;span&gt;\({e.minute}' \){e.player} (\({e.team}) - \){e.description || e.type}&lt;/span&gt;
                &lt;/div&gt;
              `).join('')}
            &lt;/div&gt;
          ` : ''}
        &lt;/div&gt;
      `).join('');
    }

    function getEventIcon(type) {
      const icons = { goal: '⚽', yellow_card: '🟨', red_card: '🟥', substitution: '🔄', penalty: '⚽' };
      return icons[type] || '•';
    }

    function renderFeed() {
      const items = feed.slice(-MAX_FEED).reverse();
      feedEl.innerHTML = items.length
        ? items.map(f =&gt; `&lt;div class="feed-item"&gt;${f}&lt;/div&gt;`).join('')
        : '&lt;div class="feed-item"&gt;Waiting for updates...&lt;/div&gt;';
    }

    function addFeedItem(type, msg) {
      const time = new Date().toLocaleTimeString();
      feed.push(`&lt;strong&gt;\({time}&lt;/strong&gt; | \){msg}`);
      if (feed.length &gt; MAX_FEED) feed.shift();
      renderFeed();
    }

    function handleMessage(data) {
      if (data.match) {
        matches.set(data.match.id, data.match);
        renderMatches();
      }
      if (data.id &amp;&amp; data.homeTeam &amp;&amp; data.awayTeam) {
        matches.set(data.id, data);
        renderMatches();
      }
      if (data.type === 'match_created' &amp;&amp; data.match) {
        addFeedItem('match', `New match: \({data.match.homeTeam} vs \){data.match.awayTeam}`);
      }
      if (data.type === 'score_update') {
        const m = matches.get(data.matchId);
        if (m) {
          m.homeScore = data.homeScore;
          m.awayScore = data.awayScore;
          m.minute = data.minute;
          m.status = data.status;
          matches.set(data.matchId, m);
          addFeedItem('score', `\({m.homeTeam} \){data.homeScore}-\({data.awayScore} \){m.awayTeam} (${data.minute || '?'}')`);
          renderMatches();
        }
      }
      if (data.type === 'match_event' &amp;&amp; data.event) {
        const m = matches.get(data.matchId);
        if (m) {
          m.events = m.events || [];
          m.events.push(data.event);
          if (data.event.type === 'goal') {
            if (data.event.team === m.homeTeam) m.homeScore++;
            else if (data.event.team === m.awayTeam) m.awayScore++;
          }
          matches.set(data.matchId, m);
          addFeedItem('event', `\({data.event.type}: \){data.event.player} (\({data.event.team}) - \){data.event.minute}'`);
          renderMatches();
        }
      }
    }

    function handleSSEMessage(msg) {
      try {
        const data = JSON.parse(msg);
        if (data.topic &amp;&amp; /^sports\/football\/match\/([^/]+)$/.test(data.topic) &amp;&amp; data.id) {
          matches.set(data.id, data);
        }
        handleMessage(data);
      } catch (e) {}
    }

    function connectSSE() {
      const es = new EventSource(`${API}/events`);
      es.onopen = () =&gt; setStatus(true);
      es.onerror = () =&gt; {
        setStatus(false);
        es.close();
        setTimeout(connectSSE, 3000);
      };
      es.onmessage = (e) =&gt; handleSSEMessage(e.data);
    }

    async function loadInitial() {
      try {
        const res = await fetch(`${API}/matches`);
        const list = await res.json();
        list.forEach(m =&gt; matches.set(m.id, m));
        renderMatches();
      } catch (e) {
        matchesEl.innerHTML = '&lt;div class="empty"&gt;&lt;p&gt;Could not load matches. Is the server running?&lt;/p&gt;&lt;/div&gt;';
      }
    }

    loadInitial();
    connectSSE();
  &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>Explanation of each part:</p>
<ul>
<li><p>The <code>matches Map</code>: stores match objects keyed by ID so updates can be applied efficiently without searching arrays.</p>
</li>
<li><p>The <code>feed array</code>: keeps a small history of recent events (limited to MAX_FEED) so the live feed remains lightweight.</p>
</li>
<li><p><code>setStatus</code>: toggles the connected class on the status dot and updates the status text to “Live” or “Reconnecting…” so users know connection status.</p>
</li>
<li><p><code>renderMatches</code>: converts the Map to a sorted array (newest first). If there are no matches, it displays an empty state. Otherwise it renders cards showing league, venue, teams, scores, status badge, minute, and events.</p>
</li>
<li><p><code>getEventIcon</code>: returns an emoji for each event type so events are visually identifiable (goal, card, substitution, and so on).</p>
</li>
<li><p><code>renderFeed</code>: displays the live feed items or a placeholder message when no updates exist.</p>
</li>
<li><p><code>addFeedItem</code>: appends a timestamped message to the feed, keeps only the latest items, and re-renders the feed.</p>
</li>
<li><p><code>handleMessage</code>: processes incoming data. It updates the matches Map for full match objects, score updates, and match events. For score updates and goals it adjusts scores and adds feed items so the viewer reflects real-time changes.</p>
</li>
<li><p><code>handleSSEMessage</code>: parses the Server-Sent Events payload and forwards it to handleMessage. If the message contains a match topic and full match data, it stores it in the Map.</p>
</li>
<li><p><code>connectSSE</code>: creates an EventSource connection to /api/events. On open it marks the connection as live. On error it closes and retries after three seconds so transient network issues do not break the stream.</p>
</li>
<li><p><code>loadInitial</code>: fetches existing matches on page load so the viewer displays data even before real-time updates arrive.</p>
</li>
</ul>
<h2 id="heading-how-to-build-the-home-page">How to Build the Home Page</h2>
<p>The home page (<code>public/index.html</code>) is a simple landing page that links to the viewer and admin interfaces. It uses a centered card layout with two buttons. The primary button (green) goes to the viewer, and the secondary button (outlined) goes to the admin. The page uses the same dark theme and Outfit font for consistency. There is no JavaScript – it's purely static HTML and CSS.</p>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html lang="en"&gt;
&lt;head&gt;
  &lt;meta charset="UTF-8"&gt;
  &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
  &lt;title&gt;Football Scores - MQTT Real-Time&lt;/title&gt;
  &lt;link rel="preconnect" href="https://fonts.googleapis.com"&gt;
  &lt;link rel="preconnect" href="https://fonts.gstatic.com" crossorigin&gt;
  &lt;link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&amp;display=swap" rel="stylesheet"&gt;
  &lt;style&gt;
    :root {
      --bg: #0a0e14;
      --surface: #131a24;
      --accent: #00d26a;
      --text: #e8edf2;
      --text-muted: #8b9aab;
    }
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: 'Outfit', sans-serif;
      background: var(--bg);
      color: var(--text);
      min-height: 100vh;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 2rem;
    }
    .card {
      background: var(--surface);
      border-radius: 16px;
      padding: 2rem;
      max-width: 400px;
      text-align: center;
      border: 1px solid rgba(255,255,255,0.06);
    }
    h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
    p { color: var(--text-muted); font-size: 0.95rem; margin-bottom: 1.5rem; }
    .links { display: flex; flex-direction: column; gap: 0.75rem; }
    a {
      display: block;
      padding: 1rem 1.5rem;
      background: var(--accent);
      color: var(--bg);
      text-decoration: none;
      font-weight: 600;
      border-radius: 10px;
      transition: opacity 0.2s;
    }
    a:hover { opacity: 0.9; }
    a.secondary {
      background: transparent;
      color: var(--text);
      border: 1px solid rgba(255,255,255,0.15);
    }
  &lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
  &lt;div class="card"&gt;
    &lt;h1&gt;⚽ Football Scores&lt;/h1&gt;
    &lt;p&gt;Real-time updates via MQTT &amp; Mosquitto&lt;/p&gt;
    &lt;div class="links"&gt;
      &lt;a href="/viewer.html"&gt;View Live Scores&lt;/a&gt;
      &lt;a href="/admin.html" class="secondary"&gt;Admin - Upload Scores&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>The Express server serves <code>index.html</code> when you visit the root URL (<code>http://localhost:3000/</code>) because the <code>express.static</code> middleware serves files from the <code>public</code> folder, and Express automatically serves <code>index.html</code> when the request path is <code>/</code>.</p>
<h2 id="heading-how-to-run-and-test-the-system">How to Run and Test the System</h2>
<p>Start the MQTT broker (Docker or local install).</p>
<p>Then start the Express server:</p>
<pre><code class="language-bash">npm start
</code></pre>
<p>Next, open <code>http://localhost:3000/admin.html</code> in the admin panel. Create a new match (for example, Manchester United vs Liverpool).</p>
<p>Open <code>http://localhost:3000/viewer.html</code> in another tab or window. Then update the score or add an event in the admin panel. The viewer should update within a second without refreshing.</p>
<h2 id="heading-how-to-extend-the-system-for-production">How to Extend the System for Production</h2>
<p>The current implementation uses in-memory storage. For production, you should:</p>
<ul>
<li><p><strong>Add a database</strong>: Store matches in PostgreSQL, MongoDB, or another database. Load matches on startup and persist every create, update, and event.</p>
</li>
<li><p><strong>Add authentication</strong>: Protect the admin routes with JSON Web Tokens (JWT) or session-based auth so only authorized users can upload scores.</p>
</li>
<li><p><strong>Add validation</strong>: Validate request bodies with a library such as Joi or Zod to prevent invalid data.</p>
</li>
<li><p><strong>Enable TLS</strong>: Use HTTPS for the Express server and secure WebSockets or MQTTS for the broker in production.</p>
</li>
<li><p><strong>Scale horizontally</strong>: If you run multiple server instances, each will have its own MQTT connection and SSE clients. The MQTT broker will deliver messages to all subscribers, so each instance will receive updates and forward them to its connected viewers.</p>
</li>
</ul>
<h2 id="heading-api-reference">API Reference</h2>
<p>For quick reference, here are the endpoints your server exposes:</p>
<table>
<thead>
<tr>
<th>Method</th>
<th>Endpoint</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td>GET</td>
<td><code>/api/matches</code></td>
<td>Returns all matches as a JSON array</td>
</tr>
<tr>
<td>POST</td>
<td><code>/api/matches</code></td>
<td>Creates a new match. Body: <code>{ homeTeam, awayTeam, league?, venue?, kickoff? }</code></td>
</tr>
<tr>
<td>PATCH</td>
<td><code>/api/matches/:id/score</code></td>
<td>Updates a match score. Body: <code>{ homeScore?, awayScore?, minute?, status? }</code></td>
</tr>
<tr>
<td>POST</td>
<td><code>/api/matches/:id/events</code></td>
<td>Adds an event. Body: <code>{ type?, team, player?, minute?, description? }</code></td>
</tr>
<tr>
<td>GET</td>
<td><code>/api/events</code></td>
<td>Server-Sent Events stream for real-time updates</td>
</tr>
</tbody></table>
<p>The <code>status</code> field can be <code>scheduled</code>, <code>live</code>, <code>halftime</code>, or <code>finished</code>. The <code>type</code> field for events can be <code>goal</code>, <code>yellow_card</code>, <code>red_card</code>, <code>substitution</code>, or <code>penalty</code>.</p>
<h2 id="heading-troubleshooting">Troubleshooting</h2>
<p>You might come across various issues as you build this. Here are some common ones:</p>
<p><strong>The server cannot connect to the MQTT broker.</strong> Check that Mosquitto is running. If you use Docker, run <code>docker ps</code> to verify the container is up. If you use the public broker, ensure you have internet connectivity and that your firewall allows outbound connections on port 1883.</p>
<p><strong>The viewer does not update when you change scores.</strong> Open the browser developer tools and check the Network tab. The <code>/api/events</code> request should show a pending state (it stays open). If it fails or closes, check the server logs for errors. Ensure you are not behind a proxy that buffers or closes long-lived connections.</p>
<p><strong>Matches disappear when you restart the server.</strong> The current implementation stores matches in memory. Restarting the server clears the data. Add a database as described in the production section to persist matches across restarts.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you built a real-time football sports update system using MQTT, Mosquitto, and Express. You've learned how to:</p>
<ul>
<li><p>Connect an Express server to an MQTT broker</p>
</li>
<li><p>Publish match and score updates to MQTT topics</p>
</li>
<li><p>Subscribe to topics and forward messages to browsers via Server-Sent Events</p>
</li>
<li><p>Build an admin interface for creating matches and updating scores</p>
</li>
<li><p>Build a viewer interface that displays live updates without page refreshes</p>
</li>
</ul>
<p>The same pattern applies to other real-time systems: IoT dashboards, live notifications, collaborative editing, and more. MQTT gives you a reliable, scalable messaging layer, and Server-Sent Events gives you a simple way to push updates to web clients.</p>
<p>The code in this tutorial gives you a solid foundation. Try adding features such as match filtering by league, historical event logs, or push notifications to make the system your own.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Production-Ready Feature Flag System with Next.js and Supabase ]]>
                </title>
                <description>
                    <![CDATA[ Feature flags are powerful tools that let you control which features are visible to users without deploying new code. They enable gradual rollouts, A/B testing, and instant feature toggles, which are all essential for modern software development. In ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-production-ready-feature-flag-system-with-nextjs-and-supabase/</link>
                <guid isPermaLink="false">69851c4ec8140c13f9fa09c8</guid>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ supabase ]]>
                    </category>
                
                    <category>
                        <![CDATA[   feature flags ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ David Aniebo ]]>
                </dc:creator>
                <pubDate>Thu, 05 Feb 2026 22:40:14 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770312675718/c462d3b5-5369-45e0-ad47-c91b441fe96f.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Feature flags are powerful tools that let you control which features are visible to users without deploying new code. They enable gradual rollouts, A/B testing, and instant feature toggles, which are all essential for modern software development.</p>
<p>In this article, we’ll build a real, production-ready feature flag system, not just a simple boolean toggle.</p>
<p>Specifically, we’ll implement:</p>
<ul>
<li><p>A global on/off flag to enable or disable features instantly</p>
</li>
<li><p>User-specific flags to grant access to individual users (for beta testing or internal users)</p>
</li>
<li><p>Percentage-based rollouts to gradually expose features to a subset of users</p>
</li>
<li><p>A React-powered admin dashboard to manage flags without redeploying</p>
</li>
<li><p>Client-side and server-side enforcement, so features are gated consistently everywhere</p>
</li>
</ul>
<p>By the end, we’ll finish by wiring a real Todo feature behind a feature flag, showing how entire pages and components can be safely toggled on and off in production.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-what-are-feature-flags">What Are Feature Flags?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-setup">Project Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-database-schema-design">Database Schema Design</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-setting-up-supabase">Setting Up Supabase</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-building-the-core-feature-flag-logic">Building the Core Feature Flag Logic</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-setting-up-react-query">Setting Up React Query</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-creating-the-react-hook">Creating the React Hook</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-building-the-admin-dashboard">Building the Admin Dashboard</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-implementing-a-real-world-example">Implementing a Real-World Example</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-server-side-usage">Server-Side Usage</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-why-react-query">Why React Query?</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you begin, make sure you have:</p>
<ul>
<li><p>Node.js 18 or higher installed</p>
</li>
<li><p>A basic understanding of React and Next.js</p>
</li>
<li><p>Familiarity with TypeScript</p>
</li>
<li><p>A Supabase account (free tier works perfectly)</p>
</li>
<li><p>A code editor like VS Code</p>
</li>
<li><p>Basic understanding of React Query (TanStack Query) for server state management</p>
</li>
</ul>
<h2 id="heading-what-are-feature-flags">What Are Feature Flags?</h2>
<p>Feature flags (also called feature toggles) are configuration mechanisms that let you enable or disable features in your application without changing code. Think of them as light switches for your features.</p>
<p>Here are some common use cases:</p>
<ul>
<li><p><strong>Gradual rollouts</strong>: Release a feature to 10% of users first, then gradually increase</p>
</li>
<li><p><strong>User-specific access</strong>: Enable features for beta testers or VIP users</p>
</li>
<li><p><strong>Emergency kill switches</strong>: Instantly disable a feature if something goes wrong</p>
</li>
<li><p><strong>A/B testing</strong>: Test different versions of features with different user groups</p>
</li>
</ul>
<h2 id="heading-project-setup">Project Setup</h2>
<p>Start by creating a new Next.js project with TypeScript:</p>
<pre><code class="lang-bash">npx create-next-app@latest supabase-feature-flag --typescript --tailwind --app
<span class="hljs-built_in">cd</span> supabase-feature-flag
</code></pre>
<p>Next, install the required dependencies:</p>
<pre><code class="lang-bash">npm install @supabase/ssr @supabase/supabase-js @tanstack/react-query
</code></pre>
<p>The <code>@supabase/ssr</code> package provides server-side rendering support for Supabase, which is essential for Next.js App Router. <code>@tanstack/react-query</code> provides powerful server state management with automatic caching, invalidation, and real-time updates, which is perfect for feature flags that need to reflect changes immediately without page refreshes.</p>
<h2 id="heading-database-schema-design">Database Schema Design</h2>
<p>Before writing any code, you need to design your database schema. A feature flag needs several properties:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769945095969/86610ba1-e0c8-4c0c-a500-8cdc061555ba.webp" alt="Database-schema-design" class="image--center mx-auto" width="2300" height="1246" loading="lazy"></p>
<ul>
<li><p>A unique key to identify the flag</p>
</li>
<li><p>A name and description for human readability</p>
</li>
<li><p>An enabled/disabled state</p>
</li>
<li><p>Support for user-specific access</p>
</li>
<li><p>Support for percentage-based rollouts</p>
</li>
</ul>
<p>Here's the SQL migration that creates the <code>feature_flags</code> table:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Create feature_flags table</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> feature_flags (
  <span class="hljs-keyword">id</span> <span class="hljs-keyword">UUID</span> PRIMARY <span class="hljs-keyword">KEY</span> <span class="hljs-keyword">DEFAULT</span> gen_random_uuid(),
  <span class="hljs-keyword">key</span> <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">UNIQUE</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  <span class="hljs-keyword">name</span> <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  description <span class="hljs-built_in">TEXT</span>,
  enabled <span class="hljs-built_in">BOOLEAN</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-literal">false</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  enabled_for_users JSONB <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'[]'</span>::jsonb,
  enabled_for_percent <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-number">0</span> <span class="hljs-keyword">CHECK</span> (enabled_for_percent &gt;= <span class="hljs-number">0</span> <span class="hljs-keyword">AND</span> enabled_for_percent &lt;= <span class="hljs-number">100</span>),
  metadata JSONB <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'{}'</span>::jsonb,
  created_at <span class="hljs-built_in">TIMESTAMP</span> <span class="hljs-keyword">WITH</span> <span class="hljs-built_in">TIME</span> ZONE <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">NOW</span>() <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  updated_at <span class="hljs-built_in">TIMESTAMP</span> <span class="hljs-keyword">WITH</span> <span class="hljs-built_in">TIME</span> ZONE <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">NOW</span>() <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>
);
</code></pre>
<p>Let's break down each field:</p>
<ul>
<li><p><code>id</code>: A unique identifier for each flag</p>
</li>
<li><p><code>key</code>: A unique string identifier (like "new-dashboard" or "beta-feature")</p>
</li>
<li><p><code>name</code>: A human-readable name</p>
</li>
<li><p><code>description</code>: Optional description of what the flag controls</p>
</li>
<li><p><code>enabled</code>: Global on/off switch</p>
</li>
<li><p><code>enabled_for_users</code>: JSON array of user IDs who have access</p>
</li>
<li><p><code>enabled_for_percent</code>: Percentage of users who should see the feature (0-100)</p>
</li>
<li><p><code>metadata</code>: Flexible JSON field for additional configuration</p>
</li>
<li><p><code>created_at</code> and <code>updated_at</code>: Timestamps for tracking</p>
</li>
</ul>
<p>The migration also includes indexes for performance:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Create indexes for fast lookups</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> idx_feature_flags_key <span class="hljs-keyword">ON</span> feature_flags(<span class="hljs-keyword">key</span>);
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> idx_feature_flags_enabled <span class="hljs-keyword">ON</span> feature_flags(enabled);
</code></pre>
<p>Indexes on <code>key</code> and <code>enabled</code> ensure fast queries when checking flag status.</p>
<h2 id="heading-setting-up-supabase">Setting Up Supabase</h2>
<h3 id="heading-step-1-create-a-supabase-project">Step 1: Create a Supabase Project</h3>
<p>To start, go to <a target="_blank" href="http://supabase.com">supabase.com</a> and sign up or log in. Then click on "New Project". Fill in your project details and wait for it to initialize.</p>
<h3 id="heading-step-2-run-the-migration">Step 2: Run the Migration</h3>
<p>In your Supabase dashboard, navigate to the SQL Editor:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769945875099/72595159-3301-422e-a648-49602c4088ec.png" alt="Supabase-row-level-security(RLS)-policies" class="image--center mx-auto" width="3020" height="1650" loading="lazy"></p>
<p>Then click "New Query". Copy and paste the complete migration SQL (including the indexes and RLS policies shown below) and click "Run".</p>
<p>Here's the complete migration with Row Level Security (RLS) policies:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Create feature_flags table</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> feature_flags (
  <span class="hljs-keyword">id</span> <span class="hljs-keyword">UUID</span> PRIMARY <span class="hljs-keyword">KEY</span> <span class="hljs-keyword">DEFAULT</span> gen_random_uuid(),
  <span class="hljs-keyword">key</span> <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">UNIQUE</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  <span class="hljs-keyword">name</span> <span class="hljs-built_in">TEXT</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  description <span class="hljs-built_in">TEXT</span>,
  enabled <span class="hljs-built_in">BOOLEAN</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-literal">false</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  enabled_for_users JSONB <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'[]'</span>::jsonb,
  enabled_for_percent <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-number">0</span> <span class="hljs-keyword">CHECK</span> (enabled_for_percent &gt;= <span class="hljs-number">0</span> <span class="hljs-keyword">AND</span> enabled_for_percent &lt;= <span class="hljs-number">100</span>),
  metadata JSONB <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'{}'</span>::jsonb,
  created_at <span class="hljs-built_in">TIMESTAMP</span> <span class="hljs-keyword">WITH</span> <span class="hljs-built_in">TIME</span> ZONE <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">NOW</span>() <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  updated_at <span class="hljs-built_in">TIMESTAMP</span> <span class="hljs-keyword">WITH</span> <span class="hljs-built_in">TIME</span> ZONE <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">NOW</span>() <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>
);

<span class="hljs-comment">-- Create indexes</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> idx_feature_flags_key <span class="hljs-keyword">ON</span> feature_flags(<span class="hljs-keyword">key</span>);
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> idx_feature_flags_enabled <span class="hljs-keyword">ON</span> feature_flags(enabled);

<span class="hljs-comment">-- Auto-update updated_at timestamp</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">OR</span> <span class="hljs-keyword">REPLACE</span> <span class="hljs-keyword">FUNCTION</span> update_updated_at_column()
<span class="hljs-keyword">RETURNS</span> <span class="hljs-keyword">TRIGGER</span> <span class="hljs-keyword">AS</span> $$
<span class="hljs-keyword">BEGIN</span>
  NEW.updated_at = <span class="hljs-keyword">NOW</span>();
  RETURN NEW;
<span class="hljs-keyword">END</span>;
$$ language 'plpgsql';

<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TRIGGER</span> update_feature_flags_updated_at
  <span class="hljs-keyword">BEFORE</span> <span class="hljs-keyword">UPDATE</span> <span class="hljs-keyword">ON</span> feature_flags
  <span class="hljs-keyword">FOR</span> <span class="hljs-keyword">EACH</span> <span class="hljs-keyword">ROW</span>
  <span class="hljs-keyword">EXECUTE</span> <span class="hljs-keyword">FUNCTION</span> update_updated_at_column();

<span class="hljs-comment">-- Enable Row Level Security</span>
<span class="hljs-keyword">ALTER</span> <span class="hljs-keyword">TABLE</span> feature_flags <span class="hljs-keyword">ENABLE</span> <span class="hljs-keyword">ROW</span> <span class="hljs-keyword">LEVEL</span> <span class="hljs-keyword">SECURITY</span>;

<span class="hljs-comment">-- Policy: Allow public read access</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">POLICY</span> <span class="hljs-string">"Allow public read access"</span>
  <span class="hljs-keyword">ON</span> feature_flags
  <span class="hljs-keyword">FOR</span> <span class="hljs-keyword">SELECT</span>
  <span class="hljs-keyword">USING</span> (<span class="hljs-literal">true</span>);

<span class="hljs-comment">-- Policy: Allow public write access (for admin operations)</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">POLICY</span> <span class="hljs-string">"Allow public write access"</span>
  <span class="hljs-keyword">ON</span> feature_flags
  <span class="hljs-keyword">FOR</span> <span class="hljs-keyword">ALL</span>
  <span class="hljs-keyword">USING</span> (<span class="hljs-literal">true</span>)
  <span class="hljs-keyword">WITH</span> <span class="hljs-keyword">CHECK</span> (<span class="hljs-literal">true</span>);
</code></pre>
<p>The RLS policies allow:</p>
<ul>
<li><p>Public read access: Anyone can check if a feature flag is enabled</p>
</li>
<li><p>Public write access: Allows the admin dashboard to create/update flags (in production, you'd restrict this further)</p>
</li>
</ul>
<h3 id="heading-step-3-get-your-api-credentials">Step 3: Get Your API Credentials</h3>
<p>Go to Settings and then API in your Supabase project. Copy your Project URL and Publishable Key. THen create a <code>.env.local</code> file in your project root:</p>
<pre><code class="lang-bash">NEXT_PUBLIC_SUPABASE_URL=your_project_url
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY=your_publishable_key
</code></pre>
<h2 id="heading-building-the-core-feature-flag-logic">Building the Core Feature Flag Logic</h2>
<p>Now let's build the core logic for checking feature flags. You'll create separate utilities for client-side and server-side usage.</p>
<h3 id="heading-typescript-types">TypeScript Types</h3>
<p>First, define the types you'll use throughout the application:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// types/feature-flag.ts</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> FeatureFlag {
  id: <span class="hljs-built_in">string</span>;
  key: <span class="hljs-built_in">string</span>;
  name: <span class="hljs-built_in">string</span>;
  description: <span class="hljs-built_in">string</span> | <span class="hljs-literal">null</span>;
  enabled: <span class="hljs-built_in">boolean</span>;
  enabled_for_users: <span class="hljs-built_in">string</span>[];
  enabled_for_percent: <span class="hljs-built_in">number</span>;
  metadata: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">any</span>&gt;;
  created_at: <span class="hljs-built_in">string</span>;
  updated_at: <span class="hljs-built_in">string</span>;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> FeatureFlagCheckResult {
  enabled: <span class="hljs-built_in">boolean</span>;
  reason?: <span class="hljs-built_in">string</span>;
}
</code></pre>
<p>The <code>FeatureFlag</code> interface matches your database schema. The <code>FeatureFlagCheckResult</code> includes a <code>reason</code> field that explains why a flag is enabled or disabled, which is useful for debugging.</p>
<h3 id="heading-supabase-client-setup">Supabase Client Setup</h3>
<p>Create the Supabase client for client-side usage:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// lib/supabase/client.ts</span>
<span class="hljs-keyword">import</span> { createBrowserClient } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/ssr"</span>;

<span class="hljs-keyword">const</span> supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
<span class="hljs-keyword">const</span> supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createClient = <span class="hljs-function">() =&gt;</span>
  createBrowserClient(supabaseUrl!, supabaseKey!);
</code></pre>
<p>The <code>createBrowserClient</code> function from <code>@supabase/ssr</code> creates a client optimized for browser usage.</p>
<p>For server-side usage:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// lib/supabase/server.ts</span>
<span class="hljs-keyword">import</span> { createServerClient, <span class="hljs-keyword">type</span> CookieOptions } <span class="hljs-keyword">from</span> <span class="hljs-string">"@supabase/ssr"</span>;
<span class="hljs-keyword">import</span> { cookies } <span class="hljs-keyword">from</span> <span class="hljs-string">"next/headers"</span>;

<span class="hljs-keyword">const</span> supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
<span class="hljs-keyword">const</span> supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> createClient = <span class="hljs-function">(<span class="hljs-params">cookieStore: ReturnType&lt;<span class="hljs-keyword">typeof</span> cookies&gt;</span>) =&gt;</span> {
  <span class="hljs-keyword">return</span> createServerClient(
    supabaseUrl!,
    supabaseKey!,
    {
      cookies: {
        getAll() {
          <span class="hljs-keyword">return</span> cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          <span class="hljs-keyword">try</span> {
            cookiesToSet.forEach(<span class="hljs-function">(<span class="hljs-params">{ name, value, options }</span>) =&gt;</span>
              cookieStore.set(name, value, options)
            )
          } <span class="hljs-keyword">catch</span> {
            <span class="hljs-comment">// The `setAll` method was called from a Server Component.</span>
            <span class="hljs-comment">// This can be ignored if you have middleware refreshing</span>
            <span class="hljs-comment">// user sessions.</span>
          }
        },
      },
    },
  );
};
</code></pre>
<p>This server client handles cookies properly for Next.js server components and API routes.</p>
<h3 id="heading-client-side-feature-flag-logic">Client-Side Feature Flag Logic</h3>
<p>Create the client-side utility for checking feature flags:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// lib/feature-flags/client.ts</span>
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/supabase/client'</span>;
<span class="hljs-keyword">import</span> { FeatureFlag, FeatureFlagCheckResult } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/types/feature-flag'</span>;

<span class="hljs-comment">// Simple cache with 5 second TTL (React Query handles primary caching)</span>
<span class="hljs-comment">// This cache is just for reducing redundant calls within a very short window</span>
<span class="hljs-keyword">const</span> cache = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Map</span>&lt;<span class="hljs-built_in">string</span>, { data: FeatureFlag | <span class="hljs-literal">null</span>; expires: <span class="hljs-built_in">number</span> }&gt;();
<span class="hljs-keyword">const</span> CACHE_TTL = <span class="hljs-number">5000</span>; <span class="hljs-comment">// 5 seconds - short enough to not interfere with React Query invalidation</span>

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getCached</span>(<span class="hljs-params">key: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">FeatureFlag</span> | <span class="hljs-title">null</span> | <span class="hljs-title">undefined</span> </span>{
  <span class="hljs-keyword">const</span> cached = cache.get(key);
  <span class="hljs-keyword">if</span> (cached &amp;&amp; cached.expires &gt; <span class="hljs-built_in">Date</span>.now()) {
    <span class="hljs-keyword">return</span> cached.data;
  }
  <span class="hljs-keyword">return</span> <span class="hljs-literal">undefined</span>;
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setCached</span>(<span class="hljs-params">key: <span class="hljs-built_in">string</span>, data: FeatureFlag | <span class="hljs-literal">null</span></span>): <span class="hljs-title">void</span> </span>{
  cache.set(key, { data, expires: <span class="hljs-built_in">Date</span>.now() + CACHE_TTL });
}
</code></pre>
<p>The cache reduces database queries by storing flag data in memory for 5 seconds. This is a secondary cache layer – React Query handles the primary caching and automatic invalidation, ensuring changes reflect immediately across all components.</p>
<p>The <code>getFeatureFlag</code> function fetches a flag from the database:</p>
<pre><code class="lang-typescript"><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">getFeatureFlag</span>(<span class="hljs-params">key: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">FeatureFlag</span> | <span class="hljs-title">null</span>&gt; </span>{
  <span class="hljs-keyword">const</span> cached = getCached(key);
  <span class="hljs-keyword">if</span> (cached !== <span class="hljs-literal">undefined</span>) <span class="hljs-keyword">return</span> cached;

  <span class="hljs-keyword">const</span> supabase = createClient();
  <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
    .from(<span class="hljs-string">'feature_flags'</span>)
    .select(<span class="hljs-string">'*'</span>)
    .eq(<span class="hljs-string">'key'</span>, key)
    .single();

  <span class="hljs-keyword">if</span> (error) {
    <span class="hljs-keyword">if</span> (error.code === <span class="hljs-string">'PGRST116'</span>) {
      setCached(key, <span class="hljs-literal">null</span>);
      <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    }
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error fetching feature flag:'</span>, error);
    <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
  }

  setCached(key, data);
  <span class="hljs-keyword">return</span> data;
}
</code></pre>
<p>The function first checks the cache. If the flag isn't cached, it queries Supabase. The error code <code>PGRST116</code> means "not found." In that case, you cache <code>null</code> to avoid repeated queries for non-existent flags.</p>
<p>The core logic is in <code>isFeatureEnabled</code>:</p>
<pre><code class="lang-typescript"><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">isFeatureEnabled</span>(<span class="hljs-params">
  key: <span class="hljs-built_in">string</span>,
  userId?: <span class="hljs-built_in">string</span>
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">FeatureFlagCheckResult</span>&gt; </span>{
  <span class="hljs-keyword">const</span> flag = <span class="hljs-keyword">await</span> getFeatureFlag(key);

  <span class="hljs-keyword">if</span> (!flag) {
    <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">false</span>, reason: <span class="hljs-string">'Flag not found'</span> };
  }

  <span class="hljs-keyword">if</span> (!flag.enabled) {
    <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">false</span>, reason: <span class="hljs-string">'Flag is globally disabled'</span> };
  }

  <span class="hljs-comment">// Check user-specific access</span>
  <span class="hljs-keyword">if</span> (userId &amp;&amp; flag.enabled_for_users.length &gt; <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">if</span> (flag.enabled_for_users.includes(userId)) {
      <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">true</span>, reason: <span class="hljs-string">'User has explicit access'</span> };
    }
    <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">false</span>, reason: <span class="hljs-string">'User not in allowed list'</span> };
  }

  <span class="hljs-comment">// Check percentage rollout</span>
  <span class="hljs-keyword">if</span> (flag.enabled_for_percent &gt; <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">const</span> hash = simpleHash(userId || key);
    <span class="hljs-keyword">const</span> percentage = hash % <span class="hljs-number">100</span>;
    <span class="hljs-keyword">const</span> enabled = percentage &lt; flag.enabled_for_percent;

    <span class="hljs-keyword">return</span> {
      enabled,
      reason: enabled
        ? <span class="hljs-string">`User falls within <span class="hljs-subst">${flag.enabled_for_percent}</span>% rollout`</span>
        : <span class="hljs-string">`User falls outside <span class="hljs-subst">${flag.enabled_for_percent}</span>% rollout`</span>,
    };
  }

  <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">true</span>, reason: <span class="hljs-string">'Flag is globally enabled'</span> };
}
</code></pre>
<p>The function follows this logic:</p>
<ol>
<li><p><strong>Flag doesn't exist</strong>: Return disabled</p>
</li>
<li><p><strong>Flag is globally disabled</strong>: Return disabled</p>
</li>
<li><p><strong>User-specific list exists</strong>: Check if the user is in the list</p>
</li>
<li><p><strong>Percentage rollout is set</strong>: Use a hash function to assign users to buckets deterministically</p>
</li>
<li><p><strong>Otherwise</strong>: Flag is globally enabled</p>
</li>
</ol>
<p>The hash function ensures consistent assignment, so that the same user always gets the same result:</p>
<pre><code class="lang-typescript"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">simpleHash</span>(<span class="hljs-params">str: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">number</span> </span>{
  <span class="hljs-keyword">let</span> hash = <span class="hljs-number">0</span>;
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; str.length; i++) {
    <span class="hljs-keyword">const</span> char = str.charCodeAt(i);
    hash = (hash &lt;&lt; <span class="hljs-number">5</span>) - hash + char;
    hash = hash &amp; hash;
  }
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">Math</span>.abs(hash);
}
</code></pre>
<p>This creates a deterministic hash, so <code>simpleHash("user-123")</code> always returns the same number, ensuring consistent feature flag decisions.</p>
<h3 id="heading-server-side-feature-flag-logic">Server-Side Feature Flag Logic</h3>
<p>The server-side version is similar but uses the server Supabase client:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// lib/feature-flags/server.ts</span>
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/supabase/server'</span>;
<span class="hljs-keyword">import</span> { cookies } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/headers'</span>;
<span class="hljs-keyword">import</span> { FeatureFlag, FeatureFlagCheckResult } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/types/feature-flag'</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">getFeatureFlag</span>(<span class="hljs-params">key: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">FeatureFlag</span> | <span class="hljs-title">null</span>&gt; </span>{
  <span class="hljs-keyword">const</span> cookieStore = <span class="hljs-keyword">await</span> cookies();
  <span class="hljs-keyword">const</span> supabase = createClient(cookieStore);

  <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
    .from(<span class="hljs-string">'feature_flags'</span>)
    .select(<span class="hljs-string">'*'</span>)
    .eq(<span class="hljs-string">'key'</span>, key)
    .single();

  <span class="hljs-keyword">if</span> (error) {
    <span class="hljs-keyword">if</span> (error.code === <span class="hljs-string">'PGRST116'</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error fetching feature flag:'</span>, error);
    <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;
  }

  <span class="hljs-keyword">return</span> data;
}

<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">isFeatureEnabled</span>(<span class="hljs-params">
  key: <span class="hljs-built_in">string</span>,
  userId?: <span class="hljs-built_in">string</span>
</span>): <span class="hljs-title">Promise</span>&lt;<span class="hljs-title">FeatureFlagCheckResult</span>&gt; </span>{
  <span class="hljs-keyword">const</span> flag = <span class="hljs-keyword">await</span> getFeatureFlag(key);

  <span class="hljs-keyword">if</span> (!flag) {
    <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">false</span>, reason: <span class="hljs-string">'Flag not found'</span> };
  }

  <span class="hljs-keyword">if</span> (!flag.enabled) {
    <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">false</span>, reason: <span class="hljs-string">'Flag is globally disabled'</span> };
  }

  <span class="hljs-comment">// Check user-specific access</span>
  <span class="hljs-keyword">if</span> (userId &amp;&amp; flag.enabled_for_users.length &gt; <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">if</span> (flag.enabled_for_users.includes(userId)) {
      <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">true</span>, reason: <span class="hljs-string">'User has explicit access'</span> };
    }
    <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">false</span>, reason: <span class="hljs-string">'User not in allowed list'</span> };
  }

  <span class="hljs-comment">// Check percentage rollout</span>
  <span class="hljs-keyword">if</span> (flag.enabled_for_percent &gt; <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">const</span> hash = simpleHash(userId || key);
    <span class="hljs-keyword">const</span> percentage = hash % <span class="hljs-number">100</span>;
    <span class="hljs-keyword">const</span> enabled = percentage &lt; flag.enabled_for_percent;

    <span class="hljs-keyword">return</span> {
      enabled,
      reason: enabled
        ? <span class="hljs-string">`User falls within <span class="hljs-subst">${flag.enabled_for_percent}</span>% rollout`</span>
        : <span class="hljs-string">`User falls outside <span class="hljs-subst">${flag.enabled_for_percent}</span>% rollout`</span>,
    };
  }

  <span class="hljs-keyword">return</span> { enabled: <span class="hljs-literal">true</span>, reason: <span class="hljs-string">'Flag is globally enabled'</span> };
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">simpleHash</span>(<span class="hljs-params">str: <span class="hljs-built_in">string</span></span>): <span class="hljs-title">number</span> </span>{
  <span class="hljs-keyword">let</span> hash = <span class="hljs-number">0</span>;
  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">let</span> i = <span class="hljs-number">0</span>; i &lt; str.length; i++) {
    <span class="hljs-keyword">const</span> char = str.charCodeAt(i);
    hash = (hash &lt;&lt; <span class="hljs-number">5</span>) - hash + char;
    hash = hash &amp; hash;
  }
  <span class="hljs-keyword">return</span> <span class="hljs-built_in">Math</span>.abs(hash);
}
</code></pre>
<p>The logic is identical to the client version, but it uses the server Supabase client that handles cookies correctly.</p>
<h2 id="heading-setting-up-react-query">Setting Up React Query</h2>
<p>We’ll rely heavily on React Query throughout this tutorial because feature flags are server-driven values that can change at runtime while users are actively using the application.</p>
<p>React Query provides robust server-state management through caching, background refetching, and cache invalidation. This allows feature flag changes to propagate automatically across the app without forcing page refreshes or manual state synchronization.</p>
<h3 id="heading-create-the-query-provider">Create the Query Provider</h3>
<p>First, create the <code>providers/QueryProvider.tsx</code> file to configure and initialize React Query for the entire application:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> { QueryClient, QueryClientProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">'@tanstack/react-query'</span>;
<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">QueryProvider</span>(<span class="hljs-params">{ children }: { children: React.ReactNode }</span>) </span>{
  <span class="hljs-keyword">const</span> [queryClient] = useState(
    <span class="hljs-function">() =&gt;</span>
      <span class="hljs-keyword">new</span> QueryClient({
        defaultOptions: {
          queries: {
            <span class="hljs-comment">// With SSR, we usually want to set some default staleTime</span>
            <span class="hljs-comment">// above 0 to avoid refetching immediately on the client</span>
            staleTime: <span class="hljs-number">20</span> * <span class="hljs-number">1000</span>,
            refetchOnWindowFocus: <span class="hljs-literal">true</span>,
            refetchOnReconnect: <span class="hljs-literal">true</span>,
          },
        },
      })
  );

  <span class="hljs-keyword">return</span> (
    &lt;QueryClientProvider client={queryClient}&gt;{children}&lt;/QueryClientProvider&gt;
  );
}
</code></pre>
<p>What’s happening in this code:</p>
<ul>
<li><p>This file runs on the client because React Query relies on React hooks, which only execute in the browser.</p>
</li>
<li><p>A single QueryClient instance is created and stored in state so it persists across renders and isn’t recreated.</p>
</li>
<li><p>The QueryClient defines how server data is cached, when it becomes stale, and when it should be refetched.</p>
</li>
</ul>
<p>This is important because components no longer need to handle fetch logic, loading states, or caching manually. Also, server data is shared and reused across components instead of being refetched repeatedly. And feature flag updates propagate automatically, keeping the app consistent without manual refreshes.</p>
<h3 id="heading-add-provider-to-root-layout">Add Provider to Root Layout</h3>
<p>Next, update the <code>app/layout.tsx</code> file to wrap the application with the React Query provider.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> <span class="hljs-keyword">type</span> { Metadata } <span class="hljs-keyword">from</span> <span class="hljs-string">'next'</span>
<span class="hljs-keyword">import</span> <span class="hljs-string">'./globals.css'</span>
<span class="hljs-keyword">import</span> { QueryProvider } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/providers/QueryProvider'</span>

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> metadata: Metadata = {
  title: <span class="hljs-string">'Feature Flag System'</span>,
  description: <span class="hljs-string">'Production-ready feature flag system with Next.js and Supabase'</span>,
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">RootLayout</span>(<span class="hljs-params">{
  children,
}: {
  children: React.ReactNode
}</span>) </span>{
  <span class="hljs-keyword">return</span> (
    &lt;html lang=<span class="hljs-string">"en"</span>&gt;
      &lt;body&gt;
        &lt;QueryProvider&gt;{children}&lt;/QueryProvider&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  )
}
</code></pre>
<p>We wrap the entire application with <code>QueryProvider</code>, so React Query can manage server data globally across the app.</p>
<h4 id="heading-what-queryprovider-actually-does">What <code>QueryProvider</code> actually does</h4>
<p><code>QueryProvider</code> creates and shares a single Query Client that is responsible for:</p>
<ul>
<li><p>Caching data fetched from the server</p>
</li>
<li><p>Tracking loading and error states</p>
</li>
<li><p>Automatically refetching data when needed</p>
</li>
<li><p>Synchronizing data between components</p>
</li>
</ul>
<p>By wrapping the app, every component inside it can use React Query hooks without any extra setup.</p>
<h2 id="heading-creating-the-react-hook">Creating the React Hook</h2>
<p>Create the <code>hooks/useFeatureFlag.ts</code> file to check whether a feature flag is enabled for a user, using React Query to cache and share the data across components:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> { useQuery } <span class="hljs-keyword">from</span> <span class="hljs-string">'@tanstack/react-query'</span>;
<span class="hljs-keyword">import</span> { isFeatureEnabled } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/feature-flags/client'</span>;
<span class="hljs-keyword">import</span> { FeatureFlagCheckResult } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/types/feature-flag'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useFeatureFlag</span>(<span class="hljs-params">key: <span class="hljs-built_in">string</span>, userId?: <span class="hljs-built_in">string</span></span>) </span>{
  <span class="hljs-keyword">const</span> {
    data: result = { enabled: <span class="hljs-literal">false</span>, reason: <span class="hljs-string">'Loading...'</span> },
    isLoading: loading,
    error,
  } = useQuery&lt;FeatureFlagCheckResult&gt;({
    queryKey: [<span class="hljs-string">'featureFlag'</span>, key, userId],
    queryFn: <span class="hljs-function">() =&gt;</span> isFeatureEnabled(key, userId),
    staleTime: <span class="hljs-number">30</span> * <span class="hljs-number">1000</span>,
    refetchOnWindowFocus: <span class="hljs-literal">true</span>,
  });

  <span class="hljs-keyword">return</span> { ...result, loading, error };
}
</code></pre>
<p>What’s happening in the code:</p>
<ul>
<li><p><code>'use client';</code> ensures this file runs in the browser because React Query hooks can only run on the client.</p>
</li>
<li><p><code>useQuery</code> fetches the feature flag status from the server and caches the result.</p>
</li>
<li><p><code>queryKey: ['featureFlag', key, userId]</code> uniquely identifies this query so React Query can cache it separately for each feature and user.</p>
</li>
<li><p><code>queryFn: () =&gt; isFeatureEnabled(key, userId)</code> is the function that actually checks if the feature is enabled.</p>
</li>
<li><p><code>staleTime: 30 * 1000</code> keeps the cached data fresh for 30 seconds before refetching.</p>
</li>
<li><p><code>refetchOnWindowFocus: true</code> automatically refetches the data when the user switches back to the tab.</p>
</li>
<li><p><code>return { ...result, loading, error }</code> makes it easy for components to access the flag status, loading state, and any errors.</p>
</li>
</ul>
<h3 id="heading-admin-hooks-for-managing-flags">Admin Hooks for Managing Flags</h3>
<p>We’re going to create a set of React Query hooks to manage feature flags from the admin dashboard. These hooks allow you to fetch, create, update, and delete feature flags while automatically keeping your UI in sync.</p>
<h4 id="heading-step-1-create-the-hooks-file">Step 1: Create the hooks file</h4>
<p>Start by creating a new file at <code>hooks/useFeatureFlags.ts</code> file. This is where we’ll implement all the hooks for the admin to manage feature flags.</p>
<p>After creating the <code>hooks/useFeatureFlags.ts</code> file, import React Query hooks and the FeatureFlag type so we can fetch, update, create, and delete feature flags with type safety and caching.</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> { useQuery, useMutation, useQueryClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@tanstack/react-query'</span>;
<span class="hljs-keyword">import</span> { FeatureFlag } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/types/feature-flag'</span>;
</code></pre>
<p>In this code:</p>
<ul>
<li><p><code>useQuery</code> fetches and caches server data automatically.</p>
</li>
<li><p><code>useMutation</code> sends updates, creates, or deletes data on the server.</p>
</li>
<li><p><code>useQueryClient</code> gives access to the query cache so we can invalidate or update queries after mutations.</p>
</li>
<li><p><code>FeatureFlag</code> is the TypeScript type definition for feature flags, ensuring our hooks use correct data structures.</p>
</li>
</ul>
<h4 id="heading-step-2-add-a-hook-to-fetch-all-feature-flags">Step 2: Add a hook to fetch all feature flags</h4>
<p>Next, in the same <code>hooks/useFeatureFlags.ts</code> file, add a hook to fetch all feature flags for the admin page. This hook will allow components to retrieve the list of flags and automatically cache the data using React Query.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useFeatureFlags</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">return</span> useQuery&lt;FeatureFlag[]&gt;({
    queryKey: [<span class="hljs-string">'featureFlags'</span>],
    queryFn: <span class="hljs-keyword">async</span> () =&gt; {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'/api/feature-flags'</span>);
      <span class="hljs-keyword">const</span> { data } = <span class="hljs-keyword">await</span> response.json();
      <span class="hljs-keyword">return</span> data || [];
    },
    staleTime: <span class="hljs-number">30</span> * <span class="hljs-number">1000</span>,
  });
}
</code></pre>
<p>What’s happening in the code:</p>
<ul>
<li><p><code>useQuery</code> fetches all feature flags from the server and caches them automatically.</p>
</li>
<li><p><code>queryKey: ['featureFlags']</code> uniquely identifies this query so React Query can manage caching and refetching.</p>
</li>
<li><p><code>queryFn</code> is an async function that calls the <code>/api/feature-flags</code> endpoint and returns the data.</p>
</li>
<li><p><code>staleTime: 30 * 1000</code> keeps the cached data fresh for 30 seconds before refetching.</p>
</li>
</ul>
<p>This matters because admin components always display the latest flags without manual refresh. Also, cached data reduces unnecessary network requests. Finally, any component using this hook will automatically update when the flags change.</p>
<h4 id="heading-step-3-add-a-hook-to-update-a-feature-flag">Step 3: Add a hook to update a feature flag</h4>
<p>Next, in the same <code>hooks/useFeatureFlags.ts</code> file, add a hook to update an existing feature flag. This hook will allow the admin to modify a flag and ensure all components using it get the updated value automatically.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useUpdateFeatureFlag</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> queryClient = useQueryClient();

  <span class="hljs-keyword">return</span> useMutation({
    mutationFn: <span class="hljs-keyword">async</span> ({
      key,
      updates,
    }: {
      key: <span class="hljs-built_in">string</span>;
      updates: Partial&lt;FeatureFlag&gt;;
    }) =&gt; {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">`/api/feature-flags/<span class="hljs-subst">${key}</span>`</span>, {
        method: <span class="hljs-string">'PATCH'</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(updates),
      });

      <span class="hljs-keyword">if</span> (!response.ok) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Failed to update feature flag'</span>);
      }

      <span class="hljs-keyword">const</span> { data } = <span class="hljs-keyword">await</span> response.json();
      <span class="hljs-keyword">return</span> data;
    },
    onSuccess: <span class="hljs-function">(<span class="hljs-params">data, variables</span>) =&gt;</span> {
      <span class="hljs-comment">// Invalidate and refetch feature flags list</span>
      queryClient.invalidateQueries({ queryKey: [<span class="hljs-string">'featureFlags'</span>] });
      <span class="hljs-comment">// Invalidate the specific feature flag check</span>
      queryClient.invalidateQueries({
        queryKey: [<span class="hljs-string">'featureFlag'</span>, variables.key],
      });
      <span class="hljs-comment">// Invalidate all feature flag checks (in case userId was involved)</span>
      queryClient.invalidateQueries({ queryKey: [<span class="hljs-string">'featureFlag'</span>] });
    },
  });
}
</code></pre>
<p>What’s happening in the code:</p>
<ul>
<li><p><code>useMutation</code> creates a function to update a feature flag on the server.</p>
</li>
<li><p><code>mutationFn</code> is an async function that sends a PATCH request to <code>/api/feature-flags/${key}</code> with the updated data.</p>
</li>
<li><p><code>onSuccess</code> runs after a successful update to <strong>invalidate cached queries</strong> so the latest data is available everywhere:</p>
<ul>
<li><p><code>['featureFlags']</code> updates the full list of flags.</p>
</li>
<li><p><code>['featureFlag', variables.key]</code> updates the specific flag that was changed.</p>
</li>
<li><p><code>['featureFlag']</code> updates any other cached flag checks (for example, per user checks).</p>
</li>
</ul>
</li>
</ul>
<h4 id="heading-step-4-add-a-hook-to-delete-a-feature-flag">Step 4: Add a hook to delete a feature flag</h4>
<p>Next, add the <code>useDeleteFeatureFlag</code> hook to delete a feature flag. This hook allows the admin to remove a flag from the system and ensures the UI updates automatically everywhere the flag was used.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useDeleteFeatureFlag</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> queryClient = useQueryClient();

  <span class="hljs-keyword">return</span> useMutation({
    mutationFn: <span class="hljs-keyword">async</span> (key: <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/feature-flags/<span class="hljs-subst">${key}</span>`</span>, {
        method: <span class="hljs-string">'DELETE'</span>,
      });

      <span class="hljs-keyword">if</span> (!response.ok) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Failed to delete feature flag'</span>);
      }
    },
    onSuccess: <span class="hljs-function">(<span class="hljs-params">_, key</span>) =&gt;</span> {
      <span class="hljs-comment">// Invalidate and refetch feature flags list</span>
      queryClient.invalidateQueries({ queryKey: [<span class="hljs-string">'featureFlags'</span>] });
      <span class="hljs-comment">// Invalidate the specific feature flag check</span>
      queryClient.invalidateQueries({ queryKey: [<span class="hljs-string">'featureFlag'</span>, key] });
    },
  });
}
</code></pre>
<p>What’s happening in the code:</p>
<ul>
<li><p><code>useMutation</code> creates a function to delete a feature flag from the server.</p>
</li>
<li><p><code>mutationFn</code> is an async function that sends a DELETE request to <code>/api/feature-flags/${key}</code>.</p>
</li>
<li><p><code>onSuccess</code> runs after the flag is successfully deleted to <strong>invalidate the cache</strong> so the UI updates:</p>
<ul>
<li><p><code>['featureFlags']</code> refetches the full list of flags.</p>
</li>
<li><p><code>['featureFlag', key]</code> removes the deleted flag from any cached queries.</p>
</li>
</ul>
</li>
</ul>
<h4 id="heading-step-5-add-a-hook-to-create-a-new-feature-flag">Step 5: Add a hook to create a new feature flag</h4>
<p>Finally, add the <code>useCreateFeatureFlag</code> hook to create a new feature flag. This allows the admin to add new flags and ensures the dashboard updates automatically when a new flag is created.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">useCreateFeatureFlag</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> queryClient = useQueryClient();

  <span class="hljs-keyword">return</span> useMutation({
    mutationFn: <span class="hljs-keyword">async</span> (flag: {
      key: <span class="hljs-built_in">string</span>;
      name: <span class="hljs-built_in">string</span>;
      description?: <span class="hljs-built_in">string</span>;
      enabled?: <span class="hljs-built_in">boolean</span>;
      enabled_for_users?: <span class="hljs-built_in">string</span>[];
      enabled_for_percent?: <span class="hljs-built_in">number</span>;
      metadata?: Record&lt;<span class="hljs-built_in">string</span>, <span class="hljs-built_in">any</span>&gt;;
    }) =&gt; {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">'/api/feature-flags'</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(flag),
      });

      <span class="hljs-keyword">if</span> (!response.ok) {
        <span class="hljs-keyword">const</span> error = <span class="hljs-keyword">await</span> response.json();
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(error.error || <span class="hljs-string">'Failed to create feature flag'</span>);
      }

      <span class="hljs-keyword">const</span> { data } = <span class="hljs-keyword">await</span> response.json();
      <span class="hljs-keyword">return</span> data;
    },
    onSuccess: <span class="hljs-function">() =&gt;</span> {
      <span class="hljs-comment">// Invalidate and refetch feature flags list</span>
      queryClient.invalidateQueries({ queryKey: [<span class="hljs-string">'featureFlags'</span>] });
    },
  });
}
</code></pre>
<p>What’s happening in the code:</p>
<ul>
<li><p><code>useMutation</code> creates a function to send a new feature flag to the server.</p>
</li>
<li><p><code>mutationFn</code> is an async function that posts the flag data to <code>/api/feature-flags</code>.</p>
</li>
<li><p><code>onSuccess</code> runs after a successful creation to <strong>invalidate the cached list of flags</strong>, so the admin dashboard shows the new flag immediately.</p>
</li>
</ul>
<h3 id="heading-feature-flag-gate-component">Feature Flag Gate Component</h3>
<p>Now we’ll create a wrapper component to conditionally render UI based on feature flags. This helps you show or hide parts of your app depending on whether a flag is enabled for a user.</p>
<p>Create the <code>components/FeatureFlagGate.tsx</code> file and add the following code:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> { ReactNode } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { useFeatureFlag } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/hooks/useFeatureFlag'</span>;

<span class="hljs-keyword">interface</span> FeatureFlagGateProps {
  flagKey: <span class="hljs-built_in">string</span>;
  userId?: <span class="hljs-built_in">string</span>;
  children: ReactNode;
  fallback?: ReactNode;
  showLoading?: ReactNode;
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">FeatureFlagGate</span>(<span class="hljs-params">{
  flagKey,
  userId,
  children,
  fallback = <span class="hljs-literal">null</span>,
  showLoading = <span class="hljs-literal">null</span>,
}: FeatureFlagGateProps</span>) </span>{
  <span class="hljs-keyword">const</span> { enabled, loading } = useFeatureFlag(flagKey, userId);

  <span class="hljs-keyword">if</span> (loading &amp;&amp; showLoading !== <span class="hljs-literal">null</span>) {
    <span class="hljs-keyword">return</span> &lt;&gt;{showLoading}&lt;/&gt;;
  }

  <span class="hljs-keyword">if</span> (!enabled) {
    <span class="hljs-keyword">return</span> &lt;&gt;{fallback}&lt;/&gt;;
  }

  <span class="hljs-keyword">return</span> &lt;&gt;{children}&lt;/&gt;;
}
</code></pre>
<p>What’s happening in the code:</p>
<ul>
<li><p><code>useFeatureFlag(flagKey, userId)</code> checks whether the feature is enabled for a specific user and tracks loading state.</p>
</li>
<li><p><code>loading &amp;&amp; showLoading !== null</code>: if the data is still loading, render the optional <code>showLoading</code> UI.</p>
</li>
<li><p><code>!enabled</code>: if the feature is disabled, render the optional <code>fallback</code> UI.</p>
</li>
<li><p><code>children</code> renders the actual content only if the flag is enabled and not loading.</p>
</li>
</ul>
<p>This makes it easy to conditionally render features without scattering logic throughout your components. It also supports custom loading and fallback UI for better user experience. And it works with React Query caching automatically, so flag changes propagate immediately.</p>
<h2 id="heading-building-the-admin-dashboard">Building the Admin Dashboard</h2>
<p>Admins need a way to manage feature flags in your app. To do this, we’ll create API routes that support CRUD operations (Create, Read, Update, Delete). These routes will interact with Supabase to store and modify flag data.</p>
<h3 id="heading-step-1-create-the-main-feature-flags-api-route">Step 1: Create the main feature flags API route</h3>
<p>Start by creating the <code>app/api/feature-flags/route.ts</code> file. This file will handle fetching all feature flags (GET) and creating new ones (POST).</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { NextRequest, NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/server'</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/supabase/server'</span>;
<span class="hljs-keyword">import</span> { cookies } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/headers'</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">request: NextRequest</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> cookieStore = <span class="hljs-keyword">await</span> cookies();
    <span class="hljs-keyword">const</span> supabase = createClient(cookieStore);
    <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">'feature_flags'</span>)
      .select(<span class="hljs-string">'*'</span>)
      .order(<span class="hljs-string">'created_at'</span>, { ascending: <span class="hljs-literal">false</span> });

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-keyword">return</span> NextResponse.json({ error: error.message }, { status: <span class="hljs-number">500</span> });
    }

    <span class="hljs-keyword">return</span> NextResponse.json({ data });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">return</span> NextResponse.json(
      { error: <span class="hljs-string">'Internal server error'</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">request: NextRequest</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> cookieStore = <span class="hljs-keyword">await</span> cookies();
    <span class="hljs-keyword">const</span> supabase = createClient(cookieStore);
    <span class="hljs-keyword">const</span> body = <span class="hljs-keyword">await</span> request.json();

    <span class="hljs-keyword">if</span> (!body.key || !body.name) {
      <span class="hljs-keyword">return</span> NextResponse.json(
        { error: <span class="hljs-string">'key and name are required'</span> },
        { status: <span class="hljs-number">400</span> }
      );
    }

    <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">'feature_flags'</span>)
      .insert({
        key: body.key,
        name: body.name,
        description: body.description || <span class="hljs-literal">null</span>,
        enabled: body.enabled || <span class="hljs-literal">false</span>,
        enabled_for_users: body.enabled_for_users || [],
        enabled_for_percent: body.enabled_for_percent || <span class="hljs-number">0</span>,
        metadata: body.metadata || {},
      })
      .select()
      .single();

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-keyword">return</span> NextResponse.json({ error: error.message }, { status: <span class="hljs-number">500</span> });
    }

    <span class="hljs-keyword">return</span> NextResponse.json({ data }, { status: <span class="hljs-number">201</span> });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">return</span> NextResponse.json(
      { error: <span class="hljs-string">'Internal server error'</span> },
      { status: <span class="hljs-number">500</span> }
    );
  }
}
</code></pre>
<p>What’s happening in this file:</p>
<ul>
<li><p><code>GET</code> fetches all feature flags from Supabase, ordered by creation date.</p>
</li>
<li><p><code>POST</code> creates a new feature flag in Supabase. It checks that <code>key</code> and <code>name</code> exist, and sets defaults for optional fields.</p>
</li>
<li><p><code>createClient(cookieStore)</code> authenticates requests with <code>Supabase</code> using cookies from the client.</p>
</li>
<li><p><code>NextResponse.json()</code> sends JSON responses with data or error messages.</p>
</li>
</ul>
<h3 id="heading-step-2-create-the-dynamic-route-for-individual-flags">Step 2: Create the dynamic route for individual flags</h3>
<p>Next, Create the file <code>app/api/feature-flags/[key]/route.ts</code>. This route handles updating (PATCH) and deleting (DELETE) individual flags based on their <code>key</code>.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { NextRequest, NextResponse } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/server'</span>;
<span class="hljs-keyword">import</span> { createClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/supabase/server'</span>;
<span class="hljs-keyword">import</span> { cookies } <span class="hljs-keyword">from</span> <span class="hljs-string">'next/headers'</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">PATCH</span>(<span class="hljs-params">
  request: NextRequest,
  { params }: { params: { key: <span class="hljs-built_in">string</span> } }
</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> cookieStore = <span class="hljs-keyword">await</span> cookies();
    <span class="hljs-keyword">const</span> supabase = createClient(cookieStore);
    <span class="hljs-keyword">const</span> body = <span class="hljs-keyword">await</span> request.json();

    <span class="hljs-keyword">const</span> { data, error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">'feature_flags'</span>)
      .update({
        ...(body.name !== <span class="hljs-literal">undefined</span> &amp;&amp; { name: body.name }),
        ...(body.description !== <span class="hljs-literal">undefined</span> &amp;&amp; { description: body.description }),
        ...(body.enabled !== <span class="hljs-literal">undefined</span> &amp;&amp; { enabled: body.enabled }),
        ...(body.enabled_for_users !== <span class="hljs-literal">undefined</span> &amp;&amp; {
          enabled_for_users: body.enabled_for_users,
        }),
        ...(body.enabled_for_percent !== <span class="hljs-literal">undefined</span> &amp;&amp; {
          enabled_for_percent: body.enabled_for_percent,
        }),
      })
      .eq(<span class="hljs-string">'key'</span>, params.key)
      .select()
      .single();

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-keyword">return</span> NextResponse.json({ error: error.message }, { status: <span class="hljs-number">500</span> });
    }

    <span class="hljs-keyword">return</span> NextResponse.json({ data });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">return</span> NextResponse.json(
      { error: <span class="hljs-string">'Internal server error'</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">DELETE</span>(<span class="hljs-params">
  request: NextRequest,
  { params }: { params: { key: <span class="hljs-built_in">string</span> } }
</span>) </span>{
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> cookieStore = <span class="hljs-keyword">await</span> cookies();
    <span class="hljs-keyword">const</span> supabase = createClient(cookieStore);

    <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase
      .from(<span class="hljs-string">'feature_flags'</span>)
      .delete()
      .eq(<span class="hljs-string">'key'</span>, params.key);

    <span class="hljs-keyword">if</span> (error) {
      <span class="hljs-keyword">return</span> NextResponse.json({ error: error.message }, { status: <span class="hljs-number">500</span> });
    }

    <span class="hljs-keyword">return</span> NextResponse.json({ success: <span class="hljs-literal">true</span> });
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-keyword">return</span> NextResponse.json(
      { error: <span class="hljs-string">'Internal server error'</span> },
      { status: <span class="hljs-number">500</span> }
    );
  }
}
</code></pre>
<p>What’s happening in this file:</p>
<ul>
<li><p><code>PATCH</code> updates a feature flag with only the fields provided in the request body.</p>
<ul>
<li>Uses the spread operator <code>(...)</code> to include only fields that exist.</li>
</ul>
</li>
<li><p><code>DELETE</code> removes the feature flag identified by <code>key</code>.</p>
</li>
<li><p><code>eq('key', params.key)</code> ensures the operation targets the correct flag.</p>
</li>
</ul>
<h3 id="heading-step-3-create-the-admin-dashboard-ui">Step 3: Create the Admin Dashboard UI</h3>
<p>Now, create the file <code>app/admin/page.tsx</code>. This is the main admin dashboard where you can view, create, and manage feature flags.</p>
<p>With React Query, the admin dashboard becomes much cleaner and automatically updates when flags change:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { FeatureFlagList } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/admin/FeatureFlagList'</span>;
<span class="hljs-keyword">import</span> { CreateFeatureFlagModal } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/admin/CreateFeatureFlagModal'</span>;
<span class="hljs-keyword">import</span> { useFeatureFlags } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/hooks/useFeatureFlags'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">AdminPage</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [showCreateModal, setShowCreateModal] = useState(<span class="hljs-literal">false</span>);
  <span class="hljs-keyword">const</span> { data: flags = [], isLoading: loading } = useFeatureFlags();

  <span class="hljs-keyword">return</span> (
    &lt;div className=<span class="hljs-string">'min-h-screen bg-gray-50 py-8'</span>&gt;
      &lt;div className=<span class="hljs-string">'max-w-7xl mx-auto px-4 sm:px-6 lg:px-8'</span>&gt;
        &lt;div className=<span class="hljs-string">'mb-8 flex justify-between items-center'</span>&gt;
          &lt;div&gt;
            &lt;h1 className=<span class="hljs-string">'text-3xl font-bold text-gray-900'</span>&gt;
              Feature Flags Admin
            &lt;/h1&gt;
            &lt;p className=<span class="hljs-string">'mt-2 text-sm text-gray-600'</span>&gt;
              Manage your feature flags and rollouts
            &lt;/p&gt;
          &lt;/div&gt;
          &lt;button
            onClick={<span class="hljs-function">() =&gt;</span> setShowCreateModal(<span class="hljs-literal">true</span>)}
            className=<span class="hljs-string">'px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors'</span>
          &gt;
            Create Feature Flag
          &lt;/button&gt;
        &lt;/div&gt;

        {loading ? (
          &lt;div className=<span class="hljs-string">'text-center py-12'</span>&gt;
            &lt;div className=<span class="hljs-string">'inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600'</span>&gt;&lt;/div&gt;
            &lt;p className=<span class="hljs-string">'mt-4 text-gray-600'</span>&gt;Loading feature flags...&lt;/p&gt;
          &lt;/div&gt;
        ) : (
          &lt;FeatureFlagList flags={flags} /&gt;
        )}

        {showCreateModal &amp;&amp; (
          &lt;CreateFeatureFlagModal
            onClose={<span class="hljs-function">() =&gt;</span> setShowCreateModal(<span class="hljs-literal">false</span>)}
            onSuccess={<span class="hljs-function">() =&gt;</span> {
              setShowCreateModal(<span class="hljs-literal">false</span>);
            }}
          /&gt;
        )}
      &lt;/div&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>What’s happening in this file:</p>
<ul>
<li><p><code>'use client';</code> marks this page as a client component so hooks like <code>useState</code> and React Query can run.</p>
</li>
<li><p><code>useState</code> manages whether the “Create Feature Flag” modal is open or closed.</p>
</li>
<li><p><code>useFeatureFlags()</code> fetches all feature flags from the API and keeps the list in sync automatically.</p>
</li>
<li><p><code>const { data: flags = [], isLoading: loading }</code> :</p>
<ul>
<li><p><code>flags</code> contains the feature flag list</p>
</li>
<li><p><code>loading</code> tracks whether the data is still being fetched</p>
</li>
</ul>
</li>
<li><p><code>loading ? ... : &lt;FeatureFlagList /&gt;</code> :</p>
<ul>
<li><p>Shows a loading spinner while flags are being fetched</p>
</li>
<li><p>Renders the feature flag table once data is available</p>
</li>
</ul>
</li>
<li><p><code>FeatureFlagList</code> displays all feature flags and their current states.</p>
</li>
</ul>
<h3 id="heading-using-feature-flags-mutations-in-components">Using Feature Flags Mutations in Components</h3>
<p>Earlier, we created mutation hooks for creating, updating, and deleting feature flags. Now let’s see how those hooks are actually used inside UI components to trigger updates and keep the interface in sync.</p>
<p>Here's how to use the mutation hooks in your components:</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>

<span class="hljs-keyword">import</span> { useState } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>
<span class="hljs-keyword">import</span> { FeatureFlag } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/types/feature-flag'</span>
<span class="hljs-keyword">import</span> { EditFeatureFlagModal } <span class="hljs-keyword">from</span> <span class="hljs-string">'./EditFeatureFlagModal'</span>
<span class="hljs-keyword">import</span> {
  useUpdateFeatureFlag,
  useDeleteFeatureFlag,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'@/hooks/useFeatureFlags'</span>

<span class="hljs-keyword">interface</span> FeatureFlagCardProps {
  flag: FeatureFlag
}

<span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">FeatureFlagCard</span>(<span class="hljs-params">{ flag }: FeatureFlagCardProps</span>) </span>{
  <span class="hljs-keyword">const</span> [isEditing, setIsEditing] = useState(<span class="hljs-literal">false</span>)
  <span class="hljs-keyword">const</span> updateFlag = useUpdateFeatureFlag()
  <span class="hljs-keyword">const</span> deleteFlag = useDeleteFeatureFlag()

  <span class="hljs-keyword">const</span> handleToggle = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> updateFlag.mutateAsync({
        key: flag.key,
        updates: { enabled: !flag.enabled },
      })
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error toggling flag:'</span>, error)
      alert(<span class="hljs-string">'Failed to toggle feature flag'</span>)
    }
  }

  <span class="hljs-keyword">const</span> handleDelete = <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">if</span> (!confirm(<span class="hljs-string">`Are you sure you want to delete "<span class="hljs-subst">${flag.name}</span>"?`</span>)) {
      <span class="hljs-keyword">return</span>
    }

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">await</span> deleteFlag.mutateAsync(flag.key)
    } <span class="hljs-keyword">catch</span> (error) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error deleting flag:'</span>, error)
      alert(<span class="hljs-string">'Failed to delete feature flag'</span>)
    }
  }

  <span class="hljs-keyword">return</span> (
    &lt;&gt;
      &lt;div className=<span class="hljs-string">"bg-white rounded-lg shadow p-6"</span>&gt;
        &lt;div className=<span class="hljs-string">"flex items-start justify-between"</span>&gt;
          &lt;div className=<span class="hljs-string">"flex-1"</span>&gt;
            &lt;div className=<span class="hljs-string">"flex items-center gap-3"</span>&gt;
              &lt;h3 className=<span class="hljs-string">"text-lg font-semibold text-gray-900"</span>&gt;
                {flag.name}
              &lt;/h3&gt;
              &lt;span
                className={<span class="hljs-string">`px-2 py-1 text-xs font-medium rounded-full <span class="hljs-subst">${
                  flag.enabled
                    ? <span class="hljs-string">'bg-green-100 text-green-800'</span>
                    : <span class="hljs-string">'bg-gray-100 text-gray-800'</span>
                }</span>`</span>}
              &gt;
                {flag.enabled ? <span class="hljs-string">'Enabled'</span> : <span class="hljs-string">'Disabled'</span>}
              &lt;/span&gt;
            &lt;/div&gt;
            &lt;p className=<span class="hljs-string">"mt-1 text-sm text-gray-600 font-mono"</span>&gt;{flag.key}&lt;/p&gt;
            {flag.description &amp;&amp; (
              &lt;p className=<span class="hljs-string">"mt-2 text-sm text-gray-500"</span>&gt;{flag.description}&lt;/p&gt;
            )}

            &lt;div className=<span class="hljs-string">"mt-4 flex flex-wrap gap-4 text-sm text-gray-600"</span>&gt;
              {flag.enabled_for_users.length &gt; <span class="hljs-number">0</span> &amp;&amp; (
                &lt;div&gt;
                  &lt;span className=<span class="hljs-string">"font-medium"</span>&gt;Users:&lt;/span&gt;{<span class="hljs-string">' '</span>}
                  {flag.enabled_for_users.length} user(s)
                &lt;/div&gt;
              )}
              {flag.enabled_for_percent &gt; <span class="hljs-number">0</span> &amp;&amp; (
                &lt;div&gt;
                  &lt;span className=<span class="hljs-string">"font-medium"</span>&gt;Rollout:&lt;/span&gt;{<span class="hljs-string">' '</span>}
                  {flag.enabled_for_percent}%
                &lt;/div&gt;
              )}
            &lt;/div&gt;
          &lt;/div&gt;

          &lt;div className=<span class="hljs-string">"flex items-center gap-2 ml-4"</span>&gt;
            &lt;button
              onClick={handleToggle}
              disabled={updateFlag.isPending}
              className={<span class="hljs-string">`px-3 py-1.5 text-sm font-medium rounded transition-colors <span class="hljs-subst">${
                flag.enabled
                  ? <span class="hljs-string">'bg-red-100 text-red-700 hover:bg-red-200'</span>
                  : <span class="hljs-string">'bg-green-100 text-green-700 hover:bg-green-200'</span>
              }</span> disabled:opacity-50`</span>}
            &gt;
              {updateFlag.isPending
                ? <span class="hljs-string">'...'</span>
                : flag.enabled
                ? <span class="hljs-string">'Disable'</span>
                : <span class="hljs-string">'Enable'</span>}
            &lt;/button&gt;
            &lt;button
              onClick={<span class="hljs-function">() =&gt;</span> setIsEditing(<span class="hljs-literal">true</span>)}
              className=<span class="hljs-string">"px-3 py-1.5 text-sm font-medium text-blue-700 bg-blue-100 rounded hover:bg-blue-200 transition-colors"</span>
            &gt;
              Edit
            &lt;/button&gt;
            &lt;button
              onClick={handleDelete}
              className=<span class="hljs-string">"px-3 py-1.5 text-sm font-medium text-red-700 bg-red-100 rounded hover:bg-red-200 transition-colors"</span>
            &gt;
              Delete
            &lt;/button&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      {isEditing &amp;&amp; (
        &lt;EditFeatureFlagModal
          flag={flag}
          onClose={<span class="hljs-function">() =&gt;</span> setIsEditing(<span class="hljs-literal">false</span>)}
          onSuccess={<span class="hljs-function">() =&gt;</span> {
            setIsEditing(<span class="hljs-literal">false</span>)
          }}
        /&gt;
      )}
    &lt;/&gt;
  )
}
</code></pre>
<p>The component above works without any manual refetching or state synchronization logic. The moment a mutation succeeds, React Query automatically updates or invalidates the relevant cached data. That behavior is what enables the smooth, real-time updates you see in the UI.</p>
<p>Because all feature flag data is managed by React Query:</p>
<ul>
<li><p>When a flag is toggled, React Query automatically invalidates the relevant queries</p>
</li>
<li><p>Any component using that flag immediately receives the updated value</p>
</li>
<li><p>The admin dashboard stays in sync with the rest of the application without extra code</p>
</li>
<li><p>Loading and error states are handled consistently across all mutations.</p>
</li>
</ul>
<h2 id="heading-implementing-a-real-world-example">Implementing a Real-World Example</h2>
<p>To make all of this concrete, let’s walk through a real-world example.</p>
<p>In this section, we’ll build a simple Todo feature that is <strong>entirely controlled by a feature flag</strong>. When the flag is disabled, users see a message explaining that the feature isn’t available. When it’s enabled, the full Todo interface appears instantly without redeploying or refreshing the page.</p>
<p>This demonstrates how feature flags can safely gate entire pages or features in a live application.</p>
<p>Below is what the experience looks like as the feature flag is toggled on and off from the admin dashboard:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769946162379/33b4a9e3-5284-477e-b8c5-ecc0e137e7c1.png" alt="feature-flag-disabled-page" class="image--center mx-auto" width="2996" height="1402" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1769946216049/edea8722-c298-4b2b-a4b5-bcc8306fb15f.png" alt="feature-flag-enabled-page" class="image--center mx-auto" width="2852" height="1536" loading="lazy"></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1770299915271/54c124ac-24d5-4d3f-825c-0507675053ee.png" alt="Todo-app-with-feature-flag" class="image--center mx-auto" width="1900" height="904" loading="lazy"></p>
<h3 id="heading-step-1-create-the-todo-page-file">Step 1: Create the Todo Page File</h3>
<p>Create a new file <code>app/todos/page.tsx</code>. This page shows how to use a feature flag to conditionally render a full component. Let's build a todo app that's controlled by a feature flag. This demonstrates real-world usage.</p>
<p>At the top of the file, import the hooks we need and define the Todo interface.</p>
<pre><code class="lang-typescript"><span class="hljs-string">'use client'</span>;

<span class="hljs-keyword">import</span> { useState, useEffect } <span class="hljs-keyword">from</span> <span class="hljs-string">'react'</span>;
<span class="hljs-keyword">import</span> { FeatureFlagGate } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/components/FeatureFlagGate'</span>;

<span class="hljs-keyword">interface</span> Todo {
  id: <span class="hljs-built_in">string</span>;
  text: <span class="hljs-built_in">string</span>;
  completed: <span class="hljs-built_in">boolean</span>;
}
</code></pre>
<h3 id="heading-step-2-initialize-state-in-the-component">Step 2: Initialize state in the component</h3>
<p>Create the component and define state for todos and the input:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TodosPage</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> [todos, setTodos] = useState&lt;Todo[]&gt;([]);
  <span class="hljs-keyword">const</span> [inputValue, setInputValue] = useState(<span class="hljs-string">''</span>);
</code></pre>
<p>Here’s what’s going on<strong>:</strong></p>
<ul>
<li><p><code>todos</code> stores all todo items.</p>
</li>
<li><p><code>inputValue</code> tracks what the user types in the input field.</p>
</li>
</ul>
<h3 id="heading-step-3-load-and-save-todos-from-localstorage">Step 3: Load and save todos from localStorage</h3>
<pre><code class="lang-typescript">  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">const</span> saved = <span class="hljs-built_in">localStorage</span>.getItem(<span class="hljs-string">'todos'</span>);
    <span class="hljs-keyword">if</span> (saved) setTodos(<span class="hljs-built_in">JSON</span>.parse(saved));
  }, []);

  useEffect(<span class="hljs-function">() =&gt;</span> {
    <span class="hljs-built_in">localStorage</span>.setItem(<span class="hljs-string">'todos'</span>, <span class="hljs-built_in">JSON</span>.stringify(todos));
  }, [todos]);
</code></pre>
<p>In this code, the first <code>useEffect</code> loads saved todos from the browser’s <code>localStorage</code> when the component mounts. The second <code>useEffect</code> saves todos whenever the list changes. This ensures your todos persist across page reloads.</p>
<h3 id="heading-step-4-add-helper-functions">Step 4: Add helper functions</h3>
<pre><code class="lang-typescript">  <span class="hljs-keyword">const</span> addTodo = <span class="hljs-function">() =&gt;</span> {
    <span class="hljs-keyword">if</span> (inputValue.trim()) {
      setTodos([...todos, { 
        id: <span class="hljs-built_in">Date</span>.now().toString(), 
        text: inputValue.trim(), 
        completed: <span class="hljs-literal">false</span> 
      }]);
      setInputValue(<span class="hljs-string">''</span>);
    }
  };

  <span class="hljs-keyword">const</span> toggleTodo = <span class="hljs-function">(<span class="hljs-params">id: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
    setTodos(todos.map(<span class="hljs-function"><span class="hljs-params">todo</span> =&gt;</span> 
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  <span class="hljs-keyword">const</span> deleteTodo = <span class="hljs-function">(<span class="hljs-params">id: <span class="hljs-built_in">string</span></span>) =&gt;</span> {
    setTodos(todos.filter(<span class="hljs-function"><span class="hljs-params">todo</span> =&gt;</span> todo.id !== id));
  };
</code></pre>
<p>In this code,</p>
<ul>
<li><p><code>addTodo</code> adds a new todo item with a unique <code>id</code> and resets the input.</p>
</li>
<li><p><code>toggleTodo</code> marks a todo as completed or incomplete.</p>
</li>
<li><p><code>deleteTodo</code> removes a todo from the list.</p>
</li>
</ul>
<h3 id="heading-step-5-render-the-todo-app-inside-the-featureflaggate">Step 5: Render the Todo App inside the FeatureFlagGate</h3>
<pre><code class="lang-xml">  return (
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"max-w-2xl mx-auto p-8"</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-4xl font-bold mb-6"</span>&gt;</span>Todo App<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>

      <span class="hljs-tag">&lt;<span class="hljs-name">FeatureFlagGate</span>
        <span class="hljs-attr">flagKey</span>=<span class="hljs-string">"test"</span>
        <span class="hljs-attr">fallback</span>=<span class="hljs-string">{</span>
          &lt;<span class="hljs-attr">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"bg-yellow-50 border-2 border-yellow-200 rounded-lg p-6"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">h3</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"font-semibold mb-2"</span>&gt;</span>Feature Not Available<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>Enable the "test" feature flag in the admin dashboard.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        }
      &gt;
</code></pre>
<p>What’s going on here:</p>
<ul>
<li><p>We wrap the Todo app in <code>FeatureFlagGate</code>.</p>
</li>
<li><p><code>flagKey="test"</code> only shows the Todo app if this feature flag is enabled.</p>
</li>
<li><p><code>fallback</code> displays a message when the feature is disabled.</p>
</li>
</ul>
<h3 id="heading-step-6-add-the-input-field-and-add-button">Step 6: Add the input field and Add button</h3>
<pre><code class="lang-xml">        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"mb-6"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"flex gap-2"</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
              <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span>
              <span class="hljs-attr">value</span>=<span class="hljs-string">{inputValue}</span>
              <span class="hljs-attr">onChange</span>=<span class="hljs-string">{(e)</span> =&gt;</span> setInputValue(e.target.value)}
              onKeyPress={(e) =&gt; e.key === 'Enter' &amp;&amp; addTodo()}
              placeholder="What needs to be done?"
              className="flex-1 px-4 py-3 border rounded-lg"
            /&gt;
            <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
              <span class="hljs-attr">onClick</span>=<span class="hljs-string">{addTodo}</span>
              <span class="hljs-attr">className</span>=<span class="hljs-string">"px-6 py-3 bg-purple-600 text-white rounded-lg"</span>
            &gt;</span>
              Add
            <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
          <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<p>In this code,</p>
<ul>
<li><p>Input field captures new todo text.</p>
</li>
<li><p>Pressing Enter or clicking Add triggers <code>addTodo</code>.</p>
</li>
<li><p>It’s styled with Tailwind CSS for spacing and rounded borders.</p>
</li>
</ul>
<h3 id="heading-step-8-display-the-list-of-todos">Step 8: Display the list of todos</h3>
<pre><code class="lang-xml">
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">className</span>=<span class="hljs-string">"space-y-2"</span>&gt;</span>
          {todos.map((todo) =&gt; (
            <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
              <span class="hljs-attr">key</span>=<span class="hljs-string">{todo.id}</span>
              <span class="hljs-attr">className</span>=<span class="hljs-string">"flex items-center gap-3 p-4 bg-gray-50 rounded-lg"</span>
            &gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
                <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> toggleTodo(todo.id)}
                className={`w-6 h-6 rounded-full border-2 ${
                  todo.completed
                    ? 'bg-green-500 border-green-500'
                    : 'border-gray-300'
                }`}
              &gt;
                {todo.completed &amp;&amp; '✓'}
              <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">span</span> <span class="hljs-attr">className</span>=<span class="hljs-string">{todo.completed</span> ? '<span class="hljs-attr">line-through</span>' <span class="hljs-attr">:</span> ''}&gt;</span>
                {todo.text}
              <span class="hljs-tag">&lt;/<span class="hljs-name">span</span>&gt;</span>
              <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
                <span class="hljs-attr">onClick</span>=<span class="hljs-string">{()</span> =&gt;</span> deleteTodo(todo.id)}
                className="text-red-500"
              &gt;
                Delete
              <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
            <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          ))}
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">FeatureFlagGate</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  );
}
</code></pre>
<p>This code,</p>
<ul>
<li><p>Loops over <code>todos</code> to display each item.</p>
</li>
<li><p>Shows a toggle button to mark as complete, a delete button, and the todo text.</p>
</li>
<li><p>Completed todos get a <strong>line-through</strong> style.</p>
</li>
<li><p>All interaction happens <strong>inside the FeatureFlagGate</strong>, so users only see this when the flag is enabled.<br>  The entire todo app is wrapped in <code>FeatureFlagGate</code>. When the "test" flag is disabled, users see a message instead of the app. When enabled, they see the full todo interface.</p>
</li>
</ul>
<h3 id="heading-todo-app-overview">Todo App Overview</h3>
<p>This Todo app demonstrates how feature flags work in a live application. It shows how admins can enable or disable the "test" feature flag dynamically from the dashboard.</p>
<p><code>FeatureFlagGate</code> ensures the interface updates immediately when the flag changes, and entire components or pages can be toggled on or off safely using feature flags.</p>
<h2 id="heading-server-side-usage">Server-Side Usage</h2>
<p>Feature flags shouldn’t only live on the client. In many cases, you’ll want to enforce them on the server as well, especially for APIs, background jobs, or sensitive business logic.</p>
<p>In this example, we’ll protect an API route by checking a feature flag on the server before returning any data.</p>
<p>First, create the API route file <code>app/api/some-feature/route.ts</code>. This demonstrates how to check a feature flag on the server before returning data.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { isFeatureEnabled } <span class="hljs-keyword">from</span> <span class="hljs-string">'@/lib/feature-flags/server'</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">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">request: Request</span>) </span>{
  <span class="hljs-keyword">const</span> userId = request.headers.get(<span class="hljs-string">'user-id'</span>) || <span class="hljs-literal">undefined</span>;
  <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> isFeatureEnabled(<span class="hljs-string">'new-feature'</span>, userId);

  <span class="hljs-keyword">if</span> (!result.enabled) {
    <span class="hljs-keyword">return</span> NextResponse.json(
      { error: <span class="hljs-string">'Feature not available'</span> },
      { status: <span class="hljs-number">403</span> }
    );
  }

  <span class="hljs-comment">// Feature is enabled, proceed with logic</span>
  <span class="hljs-keyword">return</span> NextResponse.json({ data: <span class="hljs-string">'Feature content'</span> });
}
</code></pre>
<p>What’s happening in this file:</p>
<ul>
<li><p>File creation: <code>app/api/some-feature/route.ts</code> defines a new API route.</p>
</li>
<li><p><code>isFeatureEnabled</code>: Checks whether the <code>'new-feature'</code> flag is active for the user.</p>
</li>
<li><p>Conditional response: Returns a <code>403</code> error if the feature is disabled. Otherwise, proceeds normally.</p>
</li>
<li><p>Server-side gating: Lets you protect entire endpoints, so users only access functionality when the feature is enabled.</p>
</li>
</ul>
<h2 id="heading-why-react-query">Why React Query?</h2>
<p>Feature flags introduce a unique challenge because they must remain consistent across the entire UI even as they change dynamically. Without a dedicated server-state solution, you’d need to manually refetch data, coordinate updates between components, and handle edge cases where parts of the UI fall out of sync.</p>
<p>React Query treats feature flags as shared server state. Once fetched, flags are cached and reused across components. When an admin updates a flag, the cache is invalidated and refetched in the background, triggering immediate UI updates everywhere the flag is used. This makes React Query a natural fit for feature flags, where correctness, consistency, and real-time updates are critical.</p>
<h3 id="heading-real-world-impact">Real-World Impact</h3>
<p>Before React Query:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Had to manually refetch after every update</span>
<span class="hljs-keyword">const</span> handleToggle = <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-keyword">await</span> fetch(<span class="hljs-string">`/api/feature-flags/<span class="hljs-subst">${key}</span>`</span>, { method: <span class="hljs-string">'PATCH'</span>, ... });
  fetchFlags(); <span class="hljs-comment">// Manual refetch</span>
  <span class="hljs-comment">// Other components still show old data until page refresh</span>
};
</code></pre>
<p>After React Query:</p>
<pre><code class="lang-typescript"><span class="hljs-comment">// Automatic cache invalidation - everything updates instantly</span>
<span class="hljs-keyword">const</span> updateFlag = useUpdateFeatureFlag();
<span class="hljs-keyword">await</span> updateFlag.mutateAsync({ key, updates });
<span class="hljs-comment">// All components using this flag automatically update!</span>
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You’ve now built a complete, production-ready feature flag system using Next.js and Supabase. The system supports global toggles, user-specific access, and percentage-based rollouts, all backed by a flexible database schema.</p>
<p>Feature flags can be checked on both the client and the server, ensuring consistent behavior across UI components and API routes. With React Query handling caching and invalidation, changes made in the admin dashboard propagate instantly throughout the application without deploying or refreshing the page.</p>
<p>Feature flags are a foundational tool for modern development. They let you deploy code safely, test new ideas with real users, and react quickly when something goes wrong. With this setup in place, you can confidently extend the system with audit logs, analytics, scheduled rollouts, or deeper CI/CD integrations as your product grows.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Payroll System with Express and Monnify Using Background Jobs ]]>
                </title>
                <description>
                    <![CDATA[ Processing payroll payments is an important operation for any business. When you need to pay employees simultaneously, you can't afford to have your server hang, get blocking errors, or timeout while waiting for each payment to complete. Building a p... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-a-payroll-system-with-express-and-monnify-using-background-jobs/</link>
                <guid isPermaLink="false">69680d9ead82a9267c20097d</guid>
                
                    <category>
                        <![CDATA[ Node.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Express ]]>
                    </category>
                
                    <category>
                        <![CDATA[ TypeScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ PostgreSQL ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Docker ]]>
                    </category>
                
                    <category>
                        <![CDATA[ handbook ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ David Aniebo ]]>
                </dc:creator>
                <pubDate>Wed, 14 Jan 2026 21:41:50 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1768414407566/4384def7-fdc2-4274-888d-d5bd5bd5549b.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Processing payroll payments is an important operation for any business. When you need to pay employees simultaneously, you can't afford to have your server hang, get blocking errors, or timeout while waiting for each payment to complete.</p>
<p>Building a payroll system is an excellent way to practice real-world backend development skills. Unlike simple CRUD applications, payroll systems require you to think about:</p>
<ul>
<li><p><strong>Asynchronous processing</strong>: When you need to pay hundreds of employees, processing payments synchronously can cause your server to timeout. Background jobs with Bull and Redis allow you to handle long-running operations without blocking your API.</p>
</li>
<li><p><strong>Payment gateway integration</strong>: Working with payment APIs like Monnify teaches you how to handle external service integrations, authentication flows, webhook verification, and error handling in production systems.</p>
</li>
<li><p><strong>Data consistency</strong>: Payroll systems need to maintain accurate records. You'll learn about transaction reconciliation, idempotency, and how to handle partial failures gracefully.</p>
</li>
<li><p><strong>Production-ready patterns</strong>: This tutorial covers patterns you'll use in real applications: job queues, webhook handlers, database migrations, and proper error handling.</p>
</li>
</ul>
<p>Whether you're building a fintech application, an HR system, or just want to understand how payment processing works, the concepts in this tutorial will serve you well. The combination of Express, TypeScript, background jobs, and payment APIs represents a common stack in modern backend development.</p>
<p>In this tutorial, you’ll learn how to build a production-grade payroll engine using Express.js, TypeScript, and Monnify's payment API. You'll implement background job processing with <code>Bull</code> and <code>Redis</code> to handle bulk disbursements efficiently.</p>
<p>By the end, you will have a fully functional payroll system that can:</p>
<ul>
<li><p>Manage employee records with bank account details</p>
</li>
<li><p>Create and process payroll batches</p>
</li>
<li><p>Process bulk payments using Monnify's disbursement API</p>
</li>
<li><p>Handle payment status updates via webhooks</p>
</li>
<li><p>Reconcile transactions to ensure data consistency</p>
</li>
</ul>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-architecture-overview">Project Architecture Overview</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-configuring-docker-for-postgresql-and-redis">Configuring Docker for PostgreSQL and Redis</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-setting-up-the-database">Setting Up the Database</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-creating-database-models">Creating Database Models</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-employee-model">Employee Model</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-employee-data-structure-employee-interface">Employee Data Structure (Employee Interface)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-employee-model-class">Employee Model Class</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-auto-generating-employee-ids-generateemployeeid">Auto-Generating Employee IDs (generateEmployeeId)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-creating-an-employee-create">Creating an Employee (create)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-retrieving-all-active-employees-findall">Retrieving All Active Employees (findAll)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-retrieving-an-employee-by-database-id-findbyid">Retrieving an Employee by Database ID (findById)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-retrieving-an-employee-by-employee-identifier-findbyemployeeid">Retrieving an Employee by Employee Identifier (findByEmployeeId)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-updating-an-employee-update">Updating an Employee (update)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-soft-deleting-an-employee-delete">Soft-Deleting an Employee (delete)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-payroll-model">Payroll Model</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-payroll-status-lifecycle">Payroll Status Lifecycle</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-payroll-entity">Payroll Entity</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-payroll-item-entity">Payroll Item Entity</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-creating-a-payroll-payrollmodelcreate">Creating a Payroll (PayrollModel.create)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-fetching-payroll-records">Fetching Payroll Records</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-updating-payroll-status-payrollmodelupdatestatus">Updating Payroll Status (PayrollModel.updateStatus)</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-payrollitemmodel">PayrollItemModel</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-fetching-payroll-items-payrollitemmodelfindbypayrollid">Fetching Payroll Items (PayrollItemModel.findByPayrollId)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-fetching-a-single-payroll-item-payrollitemmodelfindbyid">Fetching a Single Payroll Item (PayrollItemModel.findById)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-updating-payroll-item-status-payrollitemmodelupdatestatus">Updating Payroll Item Status (PayrollItemModel.updateStatus)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-overall-payroll-flow">Overall Payroll Flow</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-building-the-monnify-client">Building the Monnify Client</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-configuration-and-environment-setup">Configuration and Environment Setup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-create-the-monnifyclient-class">Create the MonnifyClient Class</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-axios-client-and-request-interceptor">Axios Client and Request Interceptor</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-authenticate-with-monnify">Authenticate with Monnify</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-automatic-token-refresh-ensureauthenticated">Automatic Token Refresh (ensureAuthenticated)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-initiating-bulk-transfers">Initiating Bulk Transfers</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-authorizing-bulk-transfers-otp-validation">Authorizing Bulk Transfers (OTP Validation)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-transaction-status-lookup">Transaction Status Lookup</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-batch-details-retrieval">Batch Details Retrieval</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-wallet-balance-check">Wallet Balance Check</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-implementing-background-job-processing">Implementing Background Job Processing</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-set-up-the-payroll-processing-queue">Set Up the Payroll Processing Queue</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-queue-processor-registration">Queue Processor Registration</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-bulk-payroll-processing-flow-processbulkpayroll">Bulk Payroll Processing Flow (processBulkPayroll)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-building-the-bulk-transfer-payload">Building the Bulk Transfer Payload</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-initiating-bulk-disbursement-via-monnify">Initiating Bulk Disbursement via Monnify</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-storing-transaction-references">Storing Transaction References</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-payroll-statistics-reconciliation-updatepayrollstats">Payroll Statistics Reconciliation (updatePayrollStats)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-queue-entry-point-processpayrollitems">Queue Entry Point (processPayrollItems)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-role-in-the-overall-payroll-architecture">Role in the Overall Payroll Architecture</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-creating-the-api-controllers">Creating the API Controllers</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-controller-responsibilities">Controller Responsibilities</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-creating-an-employee-createemployee">Creating an Employee (createEmployee)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-fetching-all-employees-getallemployees">Fetching All Employees (getAllEmployees)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-fetching-a-single-employee-getemployeebyid">Fetching a Single Employee (getEmployeeById)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-updating-an-employee-updateemployee">Updating an Employee (updateEmployee)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-deleting-an-employee-deleteemployee">Deleting an Employee (deleteEmployee)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-error-handling-strategy">Error Handling Strategy</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-role-in-the-overall-payroll-system">Role in the Overall Payroll System</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-payroll-controller">Payroll Controller</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-controller-responsibilities-1">Controller Responsibilities</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-creating-a-payroll-createpayroll">Creating a Payroll (createPayroll)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-fetching-all-payrolls-getallpayrolls">Fetching All Payrolls (getAllPayrolls)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-fetching-a-payroll-with-items-getpayrollbyid">Fetching a Payroll with Items (getPayrollById)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-processing-a-payroll-processpayroll">Processing a Payroll (processPayroll)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-reconciling-payroll-payments-reconcilepayroll">Reconciling Payroll Payments (reconcilePayroll)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-payroll-statistics-update-internal-helper">Payroll Statistics Update (Internal Helper)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-fetching-payroll-status-summary-getpayrollstatus">Fetching Payroll Status Summary (getPayrollStatus)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-authorizing-bulk-transfers-authorizebulktransfer">Authorizing Bulk Transfers (authorizeBulkTransfer)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-checking-transaction-status-checktransactionstatus">Checking Transaction Status (checkTransactionStatus)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-checking-wallet-balance-getaccountbalance">Checking Wallet Balance (getAccountBalance)</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-error-handling-and-resilience">Error Handling and Resilience</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-role-in-the-overall-payroll-architecture-1">Role in the Overall Payroll Architecture</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-setting-up-webhook-handlers">Setting Up Webhook Handlers</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-wiring-up-routes">Wiring Up Routes</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-employee-routes">Employee Routes</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-payroll-routes">Payroll Routes</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-main-application-entry-point">Main Application Entry Point</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-testing-the-system">Testing the System</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-setting-up-webhooks-for-production">Setting Up Webhooks for Production</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-key-takeaways">Key Takeaways</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-references">References:</a></p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>Before you begin, make sure you have the following:</p>
<ul>
<li><p>Node.js (v18 or higher)</p>
</li>
<li><p>Docker and Docker Compose installed</p>
</li>
<li><p>A Monnify merchant account with API credentials</p>
</li>
<li><p>Basic knowledge of TypeScript and Express.js</p>
</li>
<li><p>Familiarity with REST APIs</p>
</li>
</ul>
<p>You'll also need to obtain these credentials from your Monnify dashboard:</p>
<ul>
<li><p>API Key</p>
</li>
<li><p>Secret Key</p>
</li>
<li><p>Contract Code</p>
</li>
<li><p>Webhook Secret (for verifying webhook signatures)</p>
</li>
</ul>
<h2 id="heading-project-architecture-overview">Project Architecture Overview</h2>
<p>Here's how the payroll system works:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766393228193/8626c139-776c-491b-b060-2f95a760f32b.png" alt="Payroll system working principle" class="image--center mx-auto" width="2952" height="1494" loading="lazy"></p>
<p><strong>Key components:</strong></p>
<ol>
<li><p><strong>Express API</strong>: A minimal and flexible Node.js web framework that handles HTTP requests for managing employees and payrolls. Express provides routing, middleware support, and makes it easy to build RESTful APIs.</p>
</li>
<li><p><strong>Bull Queue</strong>: A Redis-based queue library for Node.js that processes payroll jobs asynchronously in the background. Bull handles job retries, scheduling, and provides a reliable way to process long-running tasks without blocking your main application thread.</p>
</li>
<li><p><strong>Redis</strong>: An in-memory data structure store that serves as the backend for Bull queues. Redis stores job data, manages job states (pending, active, completed, failed), and enables distributed job processing across multiple workers.</p>
</li>
<li><p><strong>PostgreSQL</strong>: A relational database that persists employee records, payrolls, and payment items. PostgreSQL's ACID compliance ensures data integrity, and its support for complex queries makes it ideal for financial applications.</p>
</li>
<li><p><strong>Monnify API</strong>: A payment gateway service that handles actual money transfers to employee bank accounts. Monnify provides bulk disbursement capabilities, allowing you to process multiple payments in a single API call, which is essential for payroll systems.</p>
</li>
<li><p><strong>Webhooks</strong>: HTTP callbacks that receive real-time payment status updates from Monnify. When a payment completes or fails, Monnify sends a webhook to your server, allowing you to update your database immediately without polling.</p>
</li>
</ol>
<h2 id="heading-setting-up-the-project">Setting Up the Project</h2>
<p>In this section, we'll initialize a new Node.js project with TypeScript and install all the necessary dependencies. We'll configure TypeScript for type safety and set up the project structure that will support our payroll system.</p>
<p>First, create a new directory and initialize your project:</p>
<pre><code class="lang-bash">mkdir monnify-payroll-system
<span class="hljs-built_in">cd</span> monnify-payroll-system
npm init -y
</code></pre>
<p>Next, install the required dependencies:</p>
<pre><code class="lang-bash">npm install express cors helmet dotenv axios bull ioredis pg swagger-jsdoc swagger-ui-express express-validator
</code></pre>
<p>Then install the development dependencies:</p>
<pre><code class="lang-bash">npm install -D typescript ts-node-dev @types/node @types/express @types/cors @types/pg @types/bull
</code></pre>
<p>Create a <code>tsconfig.json</code> file:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"compilerOptions"</span>: {
    <span class="hljs-attr">"target"</span>: <span class="hljs-string">"ES2020"</span>,
    <span class="hljs-attr">"module"</span>: <span class="hljs-string">"commonjs"</span>,
    <span class="hljs-attr">"lib"</span>: [<span class="hljs-string">"ES2020"</span>],
    <span class="hljs-attr">"outDir"</span>: <span class="hljs-string">"./dist"</span>,
    <span class="hljs-attr">"rootDir"</span>: <span class="hljs-string">"./src"</span>,
    <span class="hljs-attr">"strict"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"esModuleInterop"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"skipLibCheck"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"forceConsistentCasingInFileNames"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"resolveJsonModule"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"declaration"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"declarationMap"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"sourceMap"</span>: <span class="hljs-literal">true</span>
  },
  <span class="hljs-attr">"include"</span>: [<span class="hljs-string">"src/**/*"</span>, <span class="hljs-string">"scripts/**/*"</span>],
  <span class="hljs-attr">"exclude"</span>: [<span class="hljs-string">"node_modules"</span>, <span class="hljs-string">"dist"</span>]
}
</code></pre>
<p>And update your <code>package.json</code> scripts:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"scripts"</span>: {
    <span class="hljs-attr">"build"</span>: <span class="hljs-string">"tsc"</span>,
    <span class="hljs-attr">"start"</span>: <span class="hljs-string">"node dist/index.js"</span>,
    <span class="hljs-attr">"dev"</span>: <span class="hljs-string">"ts-node-dev --respawn --transpile-only src/index.ts"</span>,
    <span class="hljs-attr">"migrate"</span>: <span class="hljs-string">"ts-node scripts/run-migrations.ts"</span>
  }
}
</code></pre>
<p>Now, create a <code>.env</code> file for your environment variables. All the Monnify env details can be gotten in this <a target="_blank" href="https://app.monnify.com/developer">route</a>:</p>
<pre><code class="lang-plaintext"># Server
PORT=3008
NODE_ENV=development

# Database
DB_HOST=localhost
DB_PORT=5433
DB_NAME=payroll_db
DB_USER=payroll_user
DB_PASSWORD=payroll_password

# Redis
REDIS_HOST=localhost
REDIS_PORT=6379

# Monnify
MONNIFY_API_KEY=your_api_key
MONNIFY_SECRET_KEY=your_secret_key
MONNIFY_BASE_URL=https://sandbox.monnify.com
MONNIFY_CONTRACT_CODE=your_contract_code
MONNIFY_WEBHOOK_SECRET=your_webhook_secret
</code></pre>
<h2 id="heading-configuring-docker-for-postgresql-and-redis">Configuring Docker for PostgreSQL and Redis</h2>
<p>Before we can start building our application, we need to set up the infrastructure services: PostgreSQL for data persistence and Redis for job queue management. Using Docker Compose makes it easy to run these services locally with a single command. This approach ensures consistency across development environments and simplifies deployment.</p>
<p>Create a <code>docker-compose.yml</code> file to set up PostgreSQL and Redis:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">services:</span>
  <span class="hljs-attr">postgres:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">postgres:15-alpine</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">monnify-payroll-db</span>
    <span class="hljs-attr">environment:</span>
      <span class="hljs-attr">POSTGRES_USER:</span> <span class="hljs-string">payroll_user</span>
      <span class="hljs-attr">POSTGRES_PASSWORD:</span> <span class="hljs-string">payroll_password</span>
      <span class="hljs-attr">POSTGRES_DB:</span> <span class="hljs-string">payroll_db</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'5433:5432'</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">postgres_data:/var/lib/postgresql/data</span>
    <span class="hljs-attr">healthcheck:</span>
      <span class="hljs-attr">test:</span> [<span class="hljs-string">'CMD-SHELL'</span>, <span class="hljs-string">'pg_isready -U payroll_user'</span>]
      <span class="hljs-attr">interval:</span> <span class="hljs-string">10s</span>
      <span class="hljs-attr">timeout:</span> <span class="hljs-string">5s</span>
      <span class="hljs-attr">retries:</span> <span class="hljs-number">5</span>

  <span class="hljs-attr">redis:</span>
    <span class="hljs-attr">image:</span> <span class="hljs-string">redis:7-alpine</span>
    <span class="hljs-attr">container_name:</span> <span class="hljs-string">monnify-payroll-redis</span>
    <span class="hljs-attr">ports:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">'6379:6379'</span>
    <span class="hljs-attr">volumes:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">redis_data:/data</span>
    <span class="hljs-attr">healthcheck:</span>
      <span class="hljs-attr">test:</span> [<span class="hljs-string">'CMD'</span>, <span class="hljs-string">'redis-cli'</span>, <span class="hljs-string">'ping'</span>]
      <span class="hljs-attr">interval:</span> <span class="hljs-string">10s</span>
      <span class="hljs-attr">timeout:</span> <span class="hljs-string">5s</span>
      <span class="hljs-attr">retries:</span> <span class="hljs-number">5</span>

<span class="hljs-attr">volumes:</span>
  <span class="hljs-attr">postgres_data:</span>
  <span class="hljs-attr">redis_data:</span>
</code></pre>
<p>Start the services:</p>
<pre><code class="lang-bash">docker-compose up -d
</code></pre>
<p>And verify that both services are running:</p>
<pre><code class="lang-bash">docker-compose ps
</code></pre>
<h2 id="heading-setting-up-the-database">Setting Up the Database</h2>
<p>Now we'll configure the database connection and create the necessary tables. We'll use a connection pool for efficient database access and create migration files to set up our schema. This approach ensures our database structure is version-controlled and can be easily reproduced.</p>
<p>Create the <code>src/config/database.ts</code> file to configure the PostgreSQL connection:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Pool, PoolConfig } <span class="hljs-keyword">from</span> <span class="hljs-string">'pg'</span>;
<span class="hljs-keyword">import</span> dotenv <span class="hljs-keyword">from</span> <span class="hljs-string">'dotenv'</span>;

dotenv.config();

<span class="hljs-keyword">const</span> dbName = (process.env.DB_NAME || <span class="hljs-string">'payroll_db'</span>).trim();
<span class="hljs-keyword">if</span> (!dbName) {
  <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Database name (DB_NAME) must be set and cannot be empty'</span>);
}

<span class="hljs-keyword">const</span> config: PoolConfig = {
  host: process.env.DB_HOST || <span class="hljs-string">'localhost'</span>,
  port: <span class="hljs-built_in">parseInt</span>(process.env.DB_PORT || <span class="hljs-string">'5433'</span>),
  database: dbName,
  user: process.env.DB_USER || <span class="hljs-string">'payroll_user'</span>,
  password: process.env.DB_PASSWORD || <span class="hljs-string">'payroll_password'</span>,
  max: <span class="hljs-number">20</span>,
  idleTimeoutMillis: <span class="hljs-number">30000</span>,
  connectionTimeoutMillis: <span class="hljs-number">2000</span>,
};

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> pool = <span class="hljs-keyword">new</span> Pool(config);

pool.on(<span class="hljs-string">'error'</span>, <span class="hljs-function">(<span class="hljs-params">err: <span class="hljs-built_in">Error</span></span>) =&gt;</span> {
  <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Unexpected error on idle client'</span>, err);
  process.exit(<span class="hljs-number">-1</span>);
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> query = <span class="hljs-keyword">async</span> (text: <span class="hljs-built_in">string</span>, params?: <span class="hljs-built_in">any</span>[]) =&gt; {
  <span class="hljs-keyword">const</span> start = <span class="hljs-built_in">Date</span>.now();
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> res = <span class="hljs-keyword">await</span> pool.query(text, params);
    <span class="hljs-keyword">return</span> res;
  } <span class="hljs-keyword">catch</span> (error) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Database query error'</span>, error);
    <span class="hljs-keyword">throw</span> error;
  }
};
</code></pre>
<p>Now create the migration files. First, create a <code>migrations</code> folder:</p>
<pre><code class="lang-bash">mkdir migrations
</code></pre>
<p>Then create <code>migrations/001_create_employees_table.sql</code>:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Create employees table</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> employees (
  <span class="hljs-keyword">id</span> <span class="hljs-built_in">SERIAL</span> PRIMARY <span class="hljs-keyword">KEY</span>,
  <span class="hljs-keyword">name</span> <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">255</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  email <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">255</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span> <span class="hljs-keyword">UNIQUE</span>,
  employee_id <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">100</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span> <span class="hljs-keyword">UNIQUE</span>,
  salary <span class="hljs-built_in">DECIMAL</span>(<span class="hljs-number">15</span>, <span class="hljs-number">2</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  account_number <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">50</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  bank_code <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">20</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  bank_name <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">255</span>),
  is_active <span class="hljs-built_in">BOOLEAN</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-literal">true</span>,
  created_at <span class="hljs-built_in">TIMESTAMP</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">CURRENT_TIMESTAMP</span>,
  updated_at <span class="hljs-built_in">TIMESTAMP</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">CURRENT_TIMESTAMP</span>
);

<span class="hljs-comment">-- Create indexes for faster lookups</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> idx_employees_employee_id <span class="hljs-keyword">ON</span> employees(employee_id);
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> idx_employees_is_active <span class="hljs-keyword">ON</span> employees(is_active);
</code></pre>
<p>Now, create <code>migrations/002_create_payrolls_table.sql</code>:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Create payrolls table</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> payrolls (
  <span class="hljs-keyword">id</span> <span class="hljs-built_in">SERIAL</span> PRIMARY <span class="hljs-keyword">KEY</span>,
  payroll_period <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">100</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  total_amount <span class="hljs-built_in">DECIMAL</span>(<span class="hljs-number">15</span>, <span class="hljs-number">2</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  total_employees <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  <span class="hljs-keyword">status</span> <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">50</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'pending'</span>,
  processed_count <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-number">0</span>,
  failed_count <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-number">0</span>,
  created_at <span class="hljs-built_in">TIMESTAMP</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">CURRENT_TIMESTAMP</span>,
  updated_at <span class="hljs-built_in">TIMESTAMP</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">CURRENT_TIMESTAMP</span>,
  processed_at <span class="hljs-built_in">TIMESTAMP</span>
);

<span class="hljs-comment">-- Create indexes for faster queries</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> idx_payrolls_status <span class="hljs-keyword">ON</span> payrolls(<span class="hljs-keyword">status</span>);
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> idx_payrolls_period <span class="hljs-keyword">ON</span> payrolls(payroll_period);
</code></pre>
<p>And next, create <code>migrations/003_create_payroll_items_table.sql</code>:</p>
<pre><code class="lang-sql"><span class="hljs-comment">-- Create payroll_items table</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">TABLE</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> payroll_items (
  <span class="hljs-keyword">id</span> <span class="hljs-built_in">SERIAL</span> PRIMARY <span class="hljs-keyword">KEY</span>,
  payroll_id <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span> <span class="hljs-keyword">REFERENCES</span> payrolls(<span class="hljs-keyword">id</span>) <span class="hljs-keyword">ON</span> <span class="hljs-keyword">DELETE</span> <span class="hljs-keyword">CASCADE</span>,
  employee_id <span class="hljs-built_in">INTEGER</span> <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span> <span class="hljs-keyword">REFERENCES</span> employees(<span class="hljs-keyword">id</span>) <span class="hljs-keyword">ON</span> <span class="hljs-keyword">DELETE</span> <span class="hljs-keyword">CASCADE</span>,
  amount <span class="hljs-built_in">DECIMAL</span>(<span class="hljs-number">15</span>, <span class="hljs-number">2</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span>,
  <span class="hljs-keyword">status</span> <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">50</span>) <span class="hljs-keyword">NOT</span> <span class="hljs-literal">NULL</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-string">'pending'</span>,
  transaction_reference <span class="hljs-built_in">VARCHAR</span>(<span class="hljs-number">255</span>),
  error_message <span class="hljs-built_in">TEXT</span>,
  processed_at <span class="hljs-built_in">TIMESTAMP</span>,
  created_at <span class="hljs-built_in">TIMESTAMP</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">CURRENT_TIMESTAMP</span>,
  updated_at <span class="hljs-built_in">TIMESTAMP</span> <span class="hljs-keyword">DEFAULT</span> <span class="hljs-keyword">CURRENT_TIMESTAMP</span>
);

<span class="hljs-comment">-- Create indexes for faster queries</span>
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> idx_payroll_items_payroll_id <span class="hljs-keyword">ON</span> payroll_items(payroll_id);
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> idx_payroll_items_employee_id <span class="hljs-keyword">ON</span> payroll_items(employee_id);
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> idx_payroll_items_status <span class="hljs-keyword">ON</span> payroll_items(<span class="hljs-keyword">status</span>);
<span class="hljs-keyword">CREATE</span> <span class="hljs-keyword">INDEX</span> <span class="hljs-keyword">IF</span> <span class="hljs-keyword">NOT</span> <span class="hljs-keyword">EXISTS</span> idx_payroll_items_transaction_ref <span class="hljs-keyword">ON</span> payroll_items(transaction_reference);
</code></pre>
<p>Then create a migration runner script at <code>scripts/run-migrations.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> fs <span class="hljs-keyword">from</span> <span class="hljs-string">'fs'</span>;
<span class="hljs-keyword">import</span> path <span class="hljs-keyword">from</span> <span class="hljs-string">'path'</span>;
<span class="hljs-keyword">import</span> { pool } <span class="hljs-keyword">from</span> <span class="hljs-string">'../src/config/database'</span>;

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">runMigrations</span>(<span class="hljs-params"></span>) </span>{
  <span class="hljs-keyword">const</span> migrationsDir = path.join(__dirname, <span class="hljs-string">'../migrations'</span>);
  <span class="hljs-keyword">const</span> files = fs.readdirSync(migrationsDir).sort();

  <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> file <span class="hljs-keyword">of</span> files) {
    <span class="hljs-keyword">if</span> (file.endsWith(<span class="hljs-string">'.sql'</span>)) {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Running migration: <span class="hljs-subst">${file}</span>`</span>);
      <span class="hljs-keyword">const</span> sql = fs.readFileSync(path.join(migrationsDir, file), <span class="hljs-string">'utf-8'</span>);
      <span class="hljs-keyword">await</span> pool.query(sql);
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Completed: <span class="hljs-subst">${file}</span>`</span>);
    }
  }

  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'All migrations completed'</span>);
  <span class="hljs-keyword">await</span> pool.end();
}

runMigrations().catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
  <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Migration failed:'</span>, err);
  process.exit(<span class="hljs-number">1</span>);
});
</code></pre>
<p>Run the migrations:</p>
<pre><code class="lang-bash">npm run migrate
</code></pre>
<h2 id="heading-creating-database-models">Creating Database Models</h2>
<p>In this section, we'll create the data access layer for our payroll system. Models encapsulate all database operations, providing a clean interface for the rest of the application. We'll build two main models: one for managing employees and another for handling payrolls and payroll items.</p>
<p>For each model, I’ll first explain its purpose and key methods, then show you the complete code implementation. This approach helps you understand what each model does before you implement it.</p>
<h3 id="heading-employee-model">Employee Model</h3>
<p>The <code>EmployeeModel</code> serves as the data-access layer for employee records. It handles creating, reading, updating, and soft-deleting employees. The model includes automatic employee ID generation (format: <code>EMP001</code>, <code>EMP002</code>, and so on) and ensures that each employee has the banking details required for payroll disbursement.</p>
<p>Start by creating a new file at <code>src/models/employee.ts</code> where we’ll implement all employee-related database logic.</p>
<p>After creating the file, import a shared database query helper to execute parameterized SQL safely.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { query } <span class="hljs-keyword">from</span> <span class="hljs-string">'../config/database'</span>;
</code></pre>
<p>This keeps raw SQL isolated from controllers and ensures protection against SQL injection.</p>
<h3 id="heading-employee-data-structure-employee-interface">Employee Data Structure (<code>Employee</code> Interface)</h3>
<p>Next, we’ll define the employee interface.</p>
<p>The <code>Employee</code> interface represents a row in the <code>employees</code> database table and captures both operational and audit fields. It includes identifying fields (`id`, <code>employee_id</code>), personal fields (`name`, <code>email</code>), payroll fields (`salary`), banking details (`account_number`, <code>bank_code</code>, <code>bank_name</code>), operational state (`is_active`), and timestamps (`created_at`, <code>updated_at</code>). The <code>is_active</code> flag is used to support soft deletion and employee deactivation without permanently removing historical payroll relationships.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> Employee {
  id: <span class="hljs-built_in">number</span>;
  name: <span class="hljs-built_in">string</span>;
  email: <span class="hljs-built_in">string</span>;
  employee_id: <span class="hljs-built_in">string</span>;
  salary: <span class="hljs-built_in">number</span>;
  account_number: <span class="hljs-built_in">string</span>;
  bank_code: <span class="hljs-built_in">string</span>;
  bank_name: <span class="hljs-built_in">string</span>;
  is_active: <span class="hljs-built_in">boolean</span>;
  created_at: <span class="hljs-built_in">Date</span>;
  updated_at: <span class="hljs-built_in">Date</span>;
}
</code></pre>
<p>Now, we’ll define the <code>CreateEmployeeInput</code> interface which represent the expected payload for creating an employee. It includes required fields such as name, email, salary, and bank details.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> CreateEmployeeInput {
  name: <span class="hljs-built_in">string</span>;
  email: <span class="hljs-built_in">string</span>;
  employee_id?: <span class="hljs-built_in">string</span>;
  salary: <span class="hljs-built_in">number</span>;
  account_number: <span class="hljs-built_in">string</span>;
  bank_code: <span class="hljs-built_in">string</span>;
  bank_name: <span class="hljs-built_in">string</span>;
}
</code></pre>
<p>The <code>employee_id</code> field is optional, allowing the system to auto-generate a unique identifier if one is not provided. This flexibility supports both automated workflows and manual HR data imports.</p>
<h3 id="heading-employee-model-class">Employee Model Class</h3>
<p>Next, we’ll define the <code>EmployeeModel</code> class.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> EmployeeModel {
  <span class="hljs-comment">// Class methods will go here</span>
}
</code></pre>
<p>This class encapsulates all database operations related to employee records. It centralizes logic for creating, retrieving, updating, and deleting employees, as well as generating unique sequential employee IDs.</p>
<h3 id="heading-auto-generating-employee-ids-generateemployeeid">Auto-Generating Employee IDs (<code>generateEmployeeId</code>)</h3>
<p>We start by creating the <code>generateEmployeeId</code> method inside the <code>EmployeeModel</code> class.</p>
<pre><code class="lang-typescript">  <span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> generateEmployeeId(): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">string</span>&gt; {
    <span class="hljs-comment">// Get the highest existing employee_id number that matches EMP### pattern</span>
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> query(
      <span class="hljs-string">`SELECT employee_id FROM employees
       WHERE employee_id LIKE 'EMP%'
       AND LENGTH(employee_id) &gt;= 4
       AND SUBSTRING(employee_id FROM 4) ~ '^[0-9]+$'
       ORDER BY CAST(SUBSTRING(employee_id FROM 4) AS INTEGER) DESC
       LIMIT 1`</span>
    );

    <span class="hljs-keyword">if</span> (result.rows.length === <span class="hljs-number">0</span>) {
      <span class="hljs-keyword">return</span> <span class="hljs-string">'EMP001'</span>;
    }

    <span class="hljs-keyword">const</span> lastId = result.rows[<span class="hljs-number">0</span>].employee_id;
    <span class="hljs-keyword">const</span> numberPart = lastId.substring(<span class="hljs-number">3</span>);
    <span class="hljs-keyword">const</span> lastNumber = <span class="hljs-built_in">parseInt</span>(numberPart, <span class="hljs-number">10</span>);

    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">isNaN</span>(lastNumber)) {
      <span class="hljs-keyword">return</span> <span class="hljs-string">'EMP001'</span>;
    }

    <span class="hljs-keyword">const</span> nextNumber = lastNumber + <span class="hljs-number">1</span>;
    <span class="hljs-comment">// Format as EMP001, EMP002, etc. (3 digits minimum)</span>
    <span class="hljs-keyword">return</span> <span class="hljs-string">`EMP<span class="hljs-subst">${nextNumber.toString().padStart(<span class="hljs-number">3</span>, <span class="hljs-string">'0'</span>)}</span>`</span>;
  }
</code></pre>
<p>The private <code>generateEmployeeId</code> method generates a unique employee identifier in a readable sequential format such as <code>EMP001</code>, <code>EMP002</code>, and so on. It queries the database for the highest existing employee ID that matches the expected pattern (<code>EMP</code> prefix followed by numeric digits), orders by the numeric suffix in descending order, and increments the latest number to produce the next ID.</p>
<p>If no matching record exists, it starts from <code>EMP001</code>. The method also protects against malformed data by returning <code>EMP001</code> if parsing fails.</p>
<p>Finally, it ensures formatting consistency by padding the number portion to at least three digits using <code>padStart(3, '0')</code>, which keeps IDs aligned and easy to sort visually.</p>
<h3 id="heading-creating-an-employee-create">Creating an Employee (<code>create</code>)</h3>
<p>Next, we’ll define the <code>create</code> method, which inserts a new employee record into the database. If the caller does not supply an <code>employee_id</code>, the method generates one automatically using <code>generateEmployeeId</code>.</p>
<pre><code class="lang-typescript">  <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> create(data: CreateEmployeeInput): <span class="hljs-built_in">Promise</span>&lt;Employee&gt; {
    <span class="hljs-comment">// Auto-generate employee_id if not provided</span>
    <span class="hljs-keyword">let</span> employeeId = data.employee_id;
    <span class="hljs-keyword">if</span> (!employeeId) {
      employeeId = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.generateEmployeeId();
    }

    <span class="hljs-comment">// Check if employee_id already exists (if manually provided)</span>
    <span class="hljs-keyword">if</span> (data.employee_id) {
      <span class="hljs-keyword">const</span> existing = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.findByEmployeeId(data.employee_id);
      <span class="hljs-keyword">if</span> (existing) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Employee ID already exists'</span>);
      }
    }

    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> query(
      <span class="hljs-string">`INSERT INTO employees (name, email, employee_id, salary, account_number, bank_code, bank_name)
       VALUES ($1, $2, $3, $4, $5, $6, $7)
       RETURNING *`</span>,
      [
        data.name,
        data.email,
        employeeId,
        data.salary,
        data.account_number,
        data.bank_code,
        data.bank_name,
      ]
    );
    <span class="hljs-keyword">return</span> result.rows[<span class="hljs-number">0</span>];
  }
</code></pre>
<p>Here’s what’s happening in the code<strong>:</strong></p>
<p>If an <code>employee_id</code> is manually provided, it validates uniqueness by checking if that ID already exists among active employees, preventing collisions and ensuring each employee has a distinct identifier. After validations, the employee is inserted into the <code>employees</code> table and the new record is returned. This method ensures every employee created has complete banking details required for payroll disbursement.</p>
<h3 id="heading-retrieving-all-active-employees-findall">Retrieving All Active Employees (<code>findAll</code>)</h3>
<p>The <code>findAll</code> method fetches all active employees from the database.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> findAll(): <span class="hljs-built_in">Promise</span>&lt;Employee[]&gt; {
  <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> query(
    <span class="hljs-string">'SELECT * FROM employees WHERE is_active = true ORDER BY created_at DESC'</span>
  );
  <span class="hljs-keyword">return</span> result.rows;
}
</code></pre>
<p>The <code>findAll</code> method returns all active employees (<code>is_active = true</code>) ordered by most recent creation date. This behavior supports common UI patterns such as HR dashboards and payroll selection screens, where only active employees should be visible by default.</p>
<h3 id="heading-retrieving-an-employee-by-database-id-findbyid">Retrieving an Employee by Database ID (<code>findById</code>)</h3>
<p>The <code>findById</code> method retrieves a single employee by the internal numeric primary key (<code>id</code>).</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> findById(id: <span class="hljs-built_in">number</span>): <span class="hljs-built_in">Promise</span>&lt;Employee | <span class="hljs-literal">null</span>&gt; {
  <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> query(<span class="hljs-string">'SELECT * FROM employees WHERE id = $1'</span>, [id]);
  <span class="hljs-keyword">return</span> result.rows[<span class="hljs-number">0</span>] || <span class="hljs-literal">null</span>;
}
</code></pre>
<p>If the employee does not exist, it returns <code>null</code>. This is typically used for internal operations such as payroll processing, updates, or admin detail views.</p>
<h3 id="heading-retrieving-an-employee-by-employee-identifier-findbyemployeeid">Retrieving an Employee by Employee Identifier (<code>findByEmployeeId</code>)</h3>
<p>The <code>findByEmployeeId</code> method retrieves an active employee using the business-friendly <code>employee_id</code> (for example, <code>EMP014</code>).</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> findByEmployeeId(employeeId: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;Employee | <span class="hljs-literal">null</span>&gt; {
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> query(
      <span class="hljs-string">'SELECT * FROM employees WHERE employee_id = $1 AND is_active = true'</span>,
      [employeeId]
    );
    <span class="hljs-keyword">return</span> result.rows[<span class="hljs-number">0</span>] || <span class="hljs-literal">null</span>;
}
</code></pre>
<p>The method filters by <code>is_active = true</code> to prevent selecting deactivated employees during operations like payroll runs or HR searches.</p>
<h3 id="heading-updating-an-employee-update">Updating an Employee (<code>update</code>)</h3>
<p>The <code>update</code> method supports partial updates by dynamically building the SQL <code>SET</code> clause based on the fields present in the update payload. It iterates through the provided properties, includes only those with defined values, and constructs a parameterized query to prevent SQL injection and preserve correctness.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> update(
    id: <span class="hljs-built_in">number</span>,
    data: Partial&lt;CreateEmployeeInput&gt;
  ): <span class="hljs-built_in">Promise</span>&lt;Employee&gt; {
    <span class="hljs-keyword">const</span> fields: <span class="hljs-built_in">string</span>[] = [];
    <span class="hljs-keyword">const</span> values: <span class="hljs-built_in">any</span>[] = [];
    <span class="hljs-keyword">let</span> paramCount = <span class="hljs-number">1</span>;

    <span class="hljs-comment">// Build dynamic update query based on provided fields</span>
    <span class="hljs-built_in">Object</span>.entries(data).forEach(<span class="hljs-function">(<span class="hljs-params">[key, value]</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (value !== <span class="hljs-literal">undefined</span>) {
        fields.push(<span class="hljs-string">`<span class="hljs-subst">${key}</span> = $<span class="hljs-subst">${paramCount}</span>`</span>);
        values.push(value);
        paramCount++;
      }
    });

    <span class="hljs-keyword">if</span> (fields.length === <span class="hljs-number">0</span>) {
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'No fields to update'</span>);
    }

    <span class="hljs-comment">// Always update the updated_at timestamp</span>
    fields.push(<span class="hljs-string">`updated_at = $<span class="hljs-subst">${paramCount}</span>`</span>);
    values.push(<span class="hljs-keyword">new</span> <span class="hljs-built_in">Date</span>());
    values.push(id);

    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> query(
      <span class="hljs-string">`UPDATE employees SET <span class="hljs-subst">${fields.join(<span class="hljs-string">', '</span>)}</span> WHERE id = $<span class="hljs-subst">${
        paramCount + <span class="hljs-number">1</span>
      }</span> RETURNING *`</span>,
      values
    );
    <span class="hljs-keyword">return</span> result.rows[<span class="hljs-number">0</span>];
  }
</code></pre>
<p>Here’s what’s happening in the code:</p>
<p>If no fields are provided, it throws an error to avoid performing a meaningless update. It also explicitly updates the <code>updated_at</code> timestamp to ensure accurate audit tracking. Finally, it returns the updated database record, making it easy for controllers to respond with the latest employee state.</p>
<h3 id="heading-soft-deleting-an-employee-delete">Soft-Deleting an Employee (<code>delete</code>)</h3>
<p>Finally, instead of permanently removing the employee record, the <code>delete</code> method performs a soft delete by setting <code>is_active = false</code> and updating the <code>updated_at</code> timestamp.</p>
<p>This approach preserves historical payroll references and audit trails while excluding inactive employees from standard queries like <code>findAll</code>. It’s especially important in payroll systems where historical payment records must remain valid and traceable even after an employee leaves the organization.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> <span class="hljs-keyword">delete</span>(id: <span class="hljs-built_in">number</span>): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
  <span class="hljs-keyword">await</span> query(
    <span class="hljs-string">'UPDATE employees SET is_active = false, updated_at = NOW() WHERE id = $1'</span>,
    [id]
  );
}
</code></pre>
<p>Key features of the employee model:</p>
<ul>
<li><p>Auto-generates sequential employee IDs if not provided</p>
</li>
<li><p>Validates employee ID uniqueness</p>
</li>
<li><p>Supports soft deletion to preserve historical payroll records</p>
</li>
<li><p>Provides methods for finding employees by database ID or employee identifier</p>
</li>
</ul>
<h3 id="heading-payroll-model">Payroll Model</h3>
<p>The <code>PayrollModel</code> manages payroll batches and individual payroll items. A payroll represents a single payment cycle (for example, "December 2024"), while payroll items represent individual employee payments within that cycle. This separation allows us to track the status of each payment independently.</p>
<p>Key features:</p>
<ul>
<li><p>Creates payroll batches with automatic calculation of totals</p>
</li>
<li><p>Supports filtering employees for selective payroll runs</p>
</li>
<li><p>Tracks status at both batch and item levels</p>
</li>
<li><p>Provides methods for reconciliation and status updates</p>
</li>
</ul>
<p>Let's implement the Payroll Model.</p>
<p>We’ll begin by creating a new file at <code>src/models/payroll.ts</code>, where we’ll implement the payroll models that encapsulate payroll batch creation, employee payment tracking, and payroll status management.</p>
<p>First, import a shared database query helper to execute parameterized SQL safely.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { query } <span class="hljs-keyword">from</span> <span class="hljs-string">'../config/database'</span>;
</code></pre>
<p>This keeps raw SQL isolated from controllers and ensures protection against SQL injection.</p>
<h3 id="heading-payroll-status-lifecycle">Payroll Status Lifecycle</h3>
<p>Next, we’ll define the <code>PayrollStatus</code> enum.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-built_in">enum</span> PayrollStatus {
 PENDING = <span class="hljs-string">'pending'</span>,
 PROCESSING = <span class="hljs-string">'processing'</span>,
 COMPLETED = <span class="hljs-string">'completed'</span>,
 FAILED = <span class="hljs-string">'failed'</span>,
 PARTIALLY_COMPLETED = <span class="hljs-string">'partially_completed'</span>,
}
</code></pre>
<p>The <code>PayrollStatus</code> enum defines all possible states for both payroll batches and individual payroll items:</p>
<ul>
<li><p><strong>PENDING</strong> – Created but not yet processed</p>
</li>
<li><p><strong>PROCESSING</strong> – Currently being processed by background workers</p>
</li>
<li><p><strong>COMPLETED</strong> – Successfully processed</p>
</li>
<li><p><strong>FAILED</strong> – Processing failed</p>
</li>
<li><p><strong>PARTIALLY_COMPLETED</strong> – Some items succeeded while others failed</p>
</li>
</ul>
<h3 id="heading-payroll-entity">Payroll Entity</h3>
<p>With the payroll status lifecycle defined, we can now define the payroll entity.</p>
<p>The <code>Payroll</code> interface represents a single payroll run, such as a monthly salary payout. It stores aggregate and audit information including the payroll period, total salary amount, total number of employees, processing status, counts of successful and failed payments, and timestamps for creation, updates, and completion.</p>
<p>Add the following interface to <code>src/models/payroll.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> Payroll {
 id: <span class="hljs-built_in">number</span>;
 payroll_period: <span class="hljs-built_in">string</span>;
 total_amount: <span class="hljs-built_in">number</span>;
 total_employees: <span class="hljs-built_in">number</span>;
 status: PayrollStatus;
 processed_count: <span class="hljs-built_in">number</span>;
 failed_count: <span class="hljs-built_in">number</span>;
 created_at: <span class="hljs-built_in">Date</span>;
 updated_at: <span class="hljs-built_in">Date</span>;
 processed_at?: <span class="hljs-built_in">Date</span>;
}
</code></pre>
<p>This entity acts as the parent record for all employee payments within a payroll cycle and is used to track overall payroll progress and outcomes.</p>
<h3 id="heading-payroll-item-entity">Payroll Item Entity</h3>
<p>Next, we’ll define the payroll item entity, which represents an individual employee payment within a payroll.</p>
<p>The <code>PayrollItem</code> tracks the employee being paid, the payment amount, its processing status, any transaction reference returned by the payment provider, error messages in case of failure, and relevant timestamps.</p>
<p>Add the following interface just below the <code>Payroll</code> interface:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> PayrollItem {
  id: <span class="hljs-built_in">number</span>;
  payroll_id: <span class="hljs-built_in">number</span>;
  employee_id: <span class="hljs-built_in">number</span>;
  amount: <span class="hljs-built_in">number</span>;
  status: PayrollStatus;
  transaction_reference?: <span class="hljs-built_in">string</span>;
  error_message?: <span class="hljs-built_in">string</span>;
  processed_at?: <span class="hljs-built_in">Date</span>;
  created_at: <span class="hljs-built_in">Date</span>;
  updated_at: <span class="hljs-built_in">Date</span>;
}
</code></pre>
<p>This structure allows individual employee payments to be retried, audited, or reconciled independently without affecting the rest of the payroll batch.</p>
<h3 id="heading-creating-a-payroll-payrollmodelcreate">Creating a Payroll (<code>PayrollModel.create</code>)</h3>
<p>Now that we’ve defined the <code>Payroll</code> and <code>PayrollItem</code> entities, we can move on to creating a payroll batch.</p>
<p>To keep our business logic organized, we’ll introduce a <code>PayrollModel</code> class. This class will be responsible for creating payroll records, calculating aggregates, and generating individual payroll items for each employee.</p>
<p>Before writing the model itself, let’s define the input required to create a payroll.</p>
<p>Add the following interface below the <code>PayrollItem</code> interface:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> CreatePayrollInput {
  payroll_period: <span class="hljs-built_in">string</span>;
  employee_ids?: <span class="hljs-built_in">number</span>[];
}
</code></pre>
<ul>
<li><p><code>payroll_period</code> identifies the payroll run (for example, <code>2025-01</code>)</p>
</li>
<li><p><code>employee_ids</code> is optional and allows us to run payroll for a subset of employees, enabling selective payouts or retries</p>
</li>
</ul>
<p>Next, create the <code>PayrollModel</code> class. This class will encapsulate all payroll-related database operations.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> PayrollModel {
<span class="hljs-comment">// Payroll model class methods will go here</span>
}
</code></pre>
<p>We’ll start by implementing the <code>create</code> method, which is responsible for creating a new payroll batch.</p>
<p>The method performs the following steps:</p>
<ol>
<li><p>Optionally filters employees if specific employee IDs are provided</p>
</li>
<li><p>Calculates aggregate payroll statistics from the employees table</p>
</li>
<li><p>Creates a payroll record with a <code>PENDING</code> status</p>
</li>
<li><p>Creates a payroll item for each eligible employee</p>
</li>
</ol>
<p>Here’s the implementation:</p>
<pre><code class="lang-typescript">  <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> create(data: CreatePayrollInput): <span class="hljs-built_in">Promise</span>&lt;Payroll&gt; {
    <span class="hljs-keyword">let</span> employeeFilter = <span class="hljs-string">''</span>;
    <span class="hljs-keyword">let</span> queryParams: <span class="hljs-built_in">any</span>[] = [];

    <span class="hljs-comment">// Build filter for selective employee payrolls</span>
    <span class="hljs-keyword">if</span> (data.employee_ids &amp;&amp; data.employee_ids.length &gt; <span class="hljs-number">0</span>) {
      employeeFilter = <span class="hljs-string">`AND id = ANY($1::int[])`</span>;
      queryParams = [data.employee_ids];
    }

    <span class="hljs-comment">// Calculate aggregate statistics from employees table</span>
    <span class="hljs-keyword">const</span> employeeStats = <span class="hljs-keyword">await</span> query(
      <span class="hljs-string">`SELECT COUNT(*) as count, COALESCE(SUM(salary), 0) as total
       FROM employees
       WHERE is_active = true <span class="hljs-subst">${employeeFilter}</span>`</span>,
      queryParams
    );

    <span class="hljs-keyword">const</span> totalEmployees = <span class="hljs-built_in">parseInt</span>(employeeStats.rows[<span class="hljs-number">0</span>].count);
    <span class="hljs-keyword">const</span> totalAmount = <span class="hljs-built_in">parseFloat</span>(employeeStats.rows[<span class="hljs-number">0</span>].total);

    <span class="hljs-comment">// Create the payroll record</span>
    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> query(
      <span class="hljs-string">`INSERT INTO payrolls (payroll_period, total_amount, total_employees, status)
       VALUES ($1, $2, $3, $4)
       RETURNING *`</span>,
      [data.payroll_period, totalAmount, totalEmployees, PayrollStatus.PENDING]
    );

    <span class="hljs-keyword">const</span> payroll = result.rows[<span class="hljs-number">0</span>];

    <span class="hljs-comment">// Create payroll items for each employee</span>
    <span class="hljs-comment">// Each item starts with PENDING status and will be processed asynchronously</span>
    <span class="hljs-keyword">const</span> employees = <span class="hljs-keyword">await</span> query(
      <span class="hljs-string">`SELECT id, salary FROM employees WHERE is_active = true <span class="hljs-subst">${employeeFilter}</span>`</span>,
      queryParams
    );

    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> employee <span class="hljs-keyword">of</span> employees.rows) {
      <span class="hljs-keyword">await</span> query(
        <span class="hljs-string">`INSERT INTO payroll_items (payroll_id, employee_id, amount, status)
         VALUES ($1, $2, $3, $4)`</span>,
        [payroll.id, employee.id, employee.salary, PayrollStatus.PENDING]
      );
    }

    <span class="hljs-keyword">return</span> payroll;
  }
</code></pre>
<p>The payroll creation process begins by determining which employees should be included. If specific employee IDs are provided, only those employees are selected – otherwise, all active employees are included. This allows the system to support both full payroll runs and selective payouts.</p>
<p>Next, the system calculates aggregate payroll statistics directly from the employees table by counting eligible employees and summing their salaries. These values are stored in a new payroll record created with a <code>PENDING</code> status.</p>
<p>Finally, a payroll item is generated for each eligible employee, with each item also initialized in a <code>PENDING</code> state. This design separates payroll setup from payment execution, allowing employee payments to be processed asynchronously and in parallel in later stages of the system.</p>
<h3 id="heading-fetching-payroll-records">Fetching Payroll Records</h3>
<p>After creating payrolls, we often need to retrieve them for administrative dashboards, reporting, and audit trails.</p>
<p>The <code>PayrollModel</code> provides two simple methods:</p>
<ol>
<li><p><code>findById</code> – Retrieves a single payroll by its unique identifier</p>
</li>
<li><p><code>findAll</code> – Retrieves all payroll records, ordered by creation date (newest first)</p>
</li>
</ol>
<p>These methods should be added <strong>below</strong> the <code>create</code> method in the <code>PayrollModel</code> class:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> findById(id: <span class="hljs-built_in">number</span>): <span class="hljs-built_in">Promise</span>&lt;Payroll | <span class="hljs-literal">null</span>&gt; {
  <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> query(<span class="hljs-string">'SELECT * FROM payrolls WHERE id = $1'</span>, [id]);
  <span class="hljs-keyword">return</span> result.rows[<span class="hljs-number">0</span>] || <span class="hljs-literal">null</span>;
}

<span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> findAll(): <span class="hljs-built_in">Promise</span>&lt;Payroll[]&gt; {
  <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> query(
    <span class="hljs-string">'SELECT * FROM payrolls ORDER BY created_at DESC'</span>
  );
  <span class="hljs-keyword">return</span> result.rows;
}
</code></pre>
<p>The <code>findById</code> method retrieves a single payroll by its identifier, while <code>findAll</code> returns all payroll records ordered by creation date.</p>
<h3 id="heading-updating-payroll-status-payrollmodelupdatestatus">Updating Payroll Status (<code>PayrollModel.updateStatus</code>)</h3>
<p>Once payroll processing begins, we need a way to track the overall status of a payroll batch. The <code>updateStatus</code> method updates the payroll record with:</p>
<ul>
<li><p>The current status (<code>PENDING</code>, <code>PROCESSING</code>, <code>COMPLETED</code>, and so on)</p>
</li>
<li><p>Optional counts of processed and failed payments</p>
</li>
<li><p>A <code>processed_at</code> timestamp automatically set for terminal states (<code>COMPLETED</code> or <code>PARTIALLY_COMPLETED</code>)</p>
</li>
</ul>
<p>Add the following method below the fetch methods in your <code>PayrollModel</code> class:</p>
<pre><code class="lang-typescript">
  <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> updateStatus(
    id: <span class="hljs-built_in">number</span>,
    status: PayrollStatus,
    processedCount?: <span class="hljs-built_in">number</span>,
    failedCount?: <span class="hljs-built_in">number</span>
  ): <span class="hljs-built_in">Promise</span>&lt;Payroll&gt; {
    <span class="hljs-keyword">const</span> updates: <span class="hljs-built_in">string</span>[] = [<span class="hljs-string">'status = $2'</span>, <span class="hljs-string">'updated_at = NOW()'</span>];
    <span class="hljs-keyword">const</span> values: <span class="hljs-built_in">any</span>[] = [id, status];

    <span class="hljs-comment">// Dynamically add processed_count if provided</span>
    <span class="hljs-keyword">if</span> (processedCount !== <span class="hljs-literal">undefined</span>) {
      updates.push(<span class="hljs-string">`processed_count = $<span class="hljs-subst">${values.length + <span class="hljs-number">1</span>}</span>`</span>);
      values.push(processedCount);
    }

    <span class="hljs-comment">// Dynamically add failed_count if provided</span>
    <span class="hljs-keyword">if</span> (failedCount !== <span class="hljs-literal">undefined</span>) {
      updates.push(<span class="hljs-string">`failed_count = $<span class="hljs-subst">${values.length + <span class="hljs-number">1</span>}</span>`</span>);
      values.push(failedCount);
    }

    <span class="hljs-comment">// Set processed_at timestamp for terminal states</span>
    <span class="hljs-keyword">if</span> (
      status === PayrollStatus.COMPLETED ||
      status === PayrollStatus.PARTIALLY_COMPLETED
    ) {
      updates.push(<span class="hljs-string">`processed_at = NOW()`</span>);
    }

    <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> query(
      <span class="hljs-string">`UPDATE payrolls SET <span class="hljs-subst">${updates.join(<span class="hljs-string">', '</span>)}</span> WHERE id = $1 RETURNING *`</span>,
      values
    );
    <span class="hljs-keyword">return</span> result.rows[<span class="hljs-number">0</span>];
  }
}
</code></pre>
<p>As payroll processing progresses, this method updates the overall payroll status along with optional counts of processed and failed payments. When a payroll reaches a terminal state such as <code>COMPLETED</code> or <code>PARTIALLY_COMPLETED</code>, the system automatically records a completion timestamp. This ensures accurate tracking of payroll execution and supports reconciliation workflows.</p>
<h2 id="heading-payrollitemmodel">PayrollItemModel</h2>
<p>After handling payroll batches with <code>PayrollModel</code>, we need a way to manage individual employee payments. This is where the <code>PayrollItemModel</code> comes in. It encapsulates database operations related to payroll items, including fetching, and updating records with employee details.</p>
<p>Start by adding a new class <strong>below</strong> <code>PayrollModel</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> PayrollItemModel {
  <span class="hljs-comment">// Methods will go here</span>
}
</code></pre>
<h3 id="heading-fetching-payroll-items-payrollitemmodelfindbypayrollid">Fetching Payroll Items (<code>PayrollItemModel.findByPayrollId</code>)</h3>
<p>Often, we want to get all payroll items for a specific payroll batch. For example, to display them on a dashboard or process them in a background worker.</p>
<p>This <code>findByPayrollId</code> method does that exactly. It retrieves all payroll items associated with a specific payroll and enriches them with employee details such as name, bank account number, and bank information through a database join.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> findByPayrollId(payrollId: <span class="hljs-built_in">number</span>): <span class="hljs-built_in">Promise</span>&lt;PayrollItem[]&gt; {
  <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> query(
    <span class="hljs-string">`SELECT
       pi.id, pi.payroll_id, pi.employee_id, pi.amount, pi.status,
       pi.transaction_reference, pi.error_message, pi.processed_at,
       pi.created_at, pi.updated_at,
       e.name as employee_name, e.employee_id as employee_identifier,
       e.account_number, e.bank_code, e.bank_name
     FROM payroll_items pi
     JOIN employees e ON pi.employee_id = e.id
     WHERE pi.payroll_id = $1
     ORDER BY pi.created_at`</span>,
      [payrollId]
    );
    <span class="hljs-comment">// Normalize numeric fields from PostgreSQL (which returns them as strings)</span>
    <span class="hljs-keyword">return</span> result.rows.map(<span class="hljs-function">(<span class="hljs-params">row</span>) =&gt;</span> ({
      ...row,
      employee_id: <span class="hljs-built_in">parseInt</span>(row.employee_id, <span class="hljs-number">10</span>),
      id: <span class="hljs-built_in">parseInt</span>(row.id, <span class="hljs-number">10</span>),
      payroll_id: <span class="hljs-built_in">parseInt</span>(row.payroll_id, <span class="hljs-number">10</span>),
      amount: <span class="hljs-built_in">parseFloat</span>(row.amount),
    }));
  }
</code></pre>
<p>Here’s what’s happening in the code:</p>
<ol>
<li><p>We use a JOIN with the <code>employees</code> table so each payroll item includes the employee’s name, account number, and bank information.</p>
</li>
<li><p>Some numeric fields may come as strings, so we convert them to proper JavaScript numbers (<code>parseInt</code> / <code>parseFloat</code>) for accurate calculations and display.</p>
</li>
<li><p>The results are ordered by creation date, which helps when rendering items in a UI or processing them sequentially.</p>
</li>
</ol>
<p>This method makes it easy to work with all items in a payroll batch while keeping the data enriched and consistent.</p>
<h3 id="heading-fetching-a-single-payroll-item-payrollitemmodelfindbyid">Fetching a Single Payroll Item (<code>PayrollItemModel.findById</code>)</h3>
<p>Sometimes, you need to look at one specific employee’s payment (for example, to retry a failed transaction or investigate an issue). The <code>findById</code> method helps in fetching a single payroll item along with the employee’s details, so you have everything you need in one place.</p>
<p>Here’s how we implement it:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> findById(id: <span class="hljs-built_in">number</span>): <span class="hljs-built_in">Promise</span>&lt;PayrollItem | <span class="hljs-literal">null</span>&gt; {
  <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> query(
    <span class="hljs-string">`SELECT
       pi.id, pi.payroll_id, pi.employee_id, pi.amount, pi.status,
       pi.transaction_reference, pi.error_message, pi.processed_at,
       pi.created_at, pi.updated_at,
       e.name as employee_name, e.employee_id as employee_identifier,
       e.account_number, e.bank_code, e.bank_name
     FROM payroll_items pi
     JOIN employees e ON pi.employee_id = e.id
     WHERE pi.id = $1`</span>,
    [id]
  );

  <span class="hljs-keyword">if</span> (result.rows.length === <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;

  <span class="hljs-keyword">const</span> row = result.rows[<span class="hljs-number">0</span>];

  <span class="hljs-comment">// Convert numeric fields to proper JavaScript numbers for easier calculations and display</span>
  <span class="hljs-keyword">return</span> {
    ...row,
    employee_id: <span class="hljs-built_in">parseInt</span>(row.employee_id, <span class="hljs-number">10</span>),
    id: <span class="hljs-built_in">parseInt</span>(row.id, <span class="hljs-number">10</span>),
    payroll_id: <span class="hljs-built_in">parseInt</span>(row.payroll_id, <span class="hljs-number">10</span>),
    amount: <span class="hljs-built_in">parseFloat</span>(row.amount),
  };
}
</code></pre>
<p>Here’s what’s happening in the code:</p>
<ul>
<li><p>We use a JOIN with the <code>employees</code> table to include employee info such as name, account number, and bank details.</p>
</li>
<li><p>If the ID doesn’t exist, the method returns <code>null</code> so you can handle missing records gracefully.</p>
</li>
<li><p>Numeric fields are converted to JavaScript numbers, making it easy to calculate totals or display amounts in the UI.</p>
</li>
</ul>
<p>This method ensures that whenever you need a single payroll item, you get a complete, ready-to-use record.</p>
<h3 id="heading-updating-payroll-item-status-payrollitemmodelupdatestatus">Updating Payroll Item Status (<code>PayrollItemModel.updateStatus</code>)</h3>
<p>As individual employee payments are processed, this method updates the payroll item’s status, stores transaction references from external payment providers, captures error messages on failure, and timestamps completion or failure events. This fine-grained tracking enables reliable retries, audits, and reconciliation with external payment systems.</p>
<p>Here’s the implementation:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> updateStatus(
  id: <span class="hljs-built_in">number</span>,
  status: PayrollStatus,
  transactionReference?: <span class="hljs-built_in">string</span>,
  errorMessage?: <span class="hljs-built_in">string</span>
): <span class="hljs-built_in">Promise</span>&lt;PayrollItem&gt; {
  <span class="hljs-keyword">const</span> updates: <span class="hljs-built_in">string</span>[] = [<span class="hljs-string">'status = $2'</span>, <span class="hljs-string">'updated_at = NOW()'</span>];
  <span class="hljs-keyword">const</span> values: <span class="hljs-built_in">any</span>[] = [id, status];

  <span class="hljs-comment">// Add transaction reference if provided (from Monnify API response)</span>
  <span class="hljs-keyword">if</span> (transactionReference) {
    updates.push(<span class="hljs-string">`transaction_reference = $<span class="hljs-subst">${values.length + <span class="hljs-number">1</span>}</span>`</span>);
    values.push(transactionReference);
  }

  <span class="hljs-comment">// Add error message if provided (from failed payment)</span>
  <span class="hljs-keyword">if</span> (errorMessage) {
    updates.push(<span class="hljs-string">`error_message = $<span class="hljs-subst">${values.length + <span class="hljs-number">1</span>}</span>`</span>);
    values.push(errorMessage);
  }

  <span class="hljs-comment">// Set processed_at timestamp for terminal states</span>
  <span class="hljs-keyword">if</span> (status === PayrollStatus.COMPLETED || status === PayrollStatus.FAILED) {
    updates.push(<span class="hljs-string">`processed_at = NOW()`</span>);
  }

  <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> query(
    <span class="hljs-string">`UPDATE payroll_items SET <span class="hljs-subst">${updates.join(
      <span class="hljs-string">', '</span>
     )}</span> WHERE id = $1 RETURNING *`</span>,
     values
   );
   <span class="hljs-keyword">return</span> result.rows[<span class="hljs-number">0</span>];
 }
}
</code></pre>
<p>Here’s what’s happening in the code:</p>
<ul>
<li><p>We build a dynamic SET clause to update only the fields provided – status is required, while transaction reference and error message are optional.</p>
</li>
<li><p>Terminal states (<code>COMPLETED</code> or <code>FAILED</code>) trigger an automatic timestamp on <code>processed_at</code>, so we always know when a payment finished.</p>
</li>
<li><p>The method returns the updated payroll item, ready for further processing, logging, or UI display.</p>
</li>
</ul>
<p>This ensures each payroll item is tracked accurately throughout its lifecycle, enabling reliable retries and complete audit trails.</p>
<h3 id="heading-overall-payroll-flow">Overall Payroll Flow</h3>
<p>In this payroll flow, an administrator creates a payroll batch, which generates individual payroll items for each employee. The payroll is then handed off to background workers that process each payroll item independently via an external payment service.</p>
<p>As each payment succeeds or fails, payroll items are updated accordingly. Once processing concludes, the payroll batch status is updated to reflect the overall outcome, whether fully successful, partially successful, or failed.</p>
<p>This architecture provides scalability, resilience, and strong auditability for real-world payroll systems.</p>
<h2 id="heading-building-the-monnify-client">Building the Monnify Client</h2>
<p>The Monnify client is the bridge between our application and Monnify's payment API. In this section, we'll build a reusable client that handles authentication, bulk transfers, and transaction tracking. The client automatically manages API tokens, retries failed requests, and provides a clean interface for the rest of our application.</p>
<p>This module implements a reusable Monnify API client responsible for handling authentication, bulk payroll disbursements, authorization, transaction tracking, and balance checks in a secure and production-ready manner. It abstracts all Monnify-specific logic behind a single class, making it easy to integrate into background jobs, payroll processors, or service layers.</p>
<p>We’ll begin by creating a new file at <code>src/config/monnify.ts</code> where we’ll implement the Monnify client.</p>
<h3 id="heading-configuration-and-environment-setup">Configuration and Environment Setup</h3>
<p>Start by loading the configuration from environment variables using <code>dotenv</code>, ensuring that sensitive credentials are never hardcoded. These include the Monnify API key, secret key, base URL, and contract code (wallet account number). This setup allows the same client to be safely used across development, staging, and production environments.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> axios, { AxiosInstance } <span class="hljs-keyword">from</span> <span class="hljs-string">'axios'</span>;
<span class="hljs-keyword">import</span> dotenv <span class="hljs-keyword">from</span> <span class="hljs-string">'dotenv'</span>;

dotenv.config();

<span class="hljs-keyword">export</span> <span class="hljs-keyword">interface</span> MonnifyConfig {
  apiKey: <span class="hljs-built_in">string</span>;
  secretKey: <span class="hljs-built_in">string</span>;
  baseUrl: <span class="hljs-built_in">string</span>;
  contractCode: <span class="hljs-built_in">string</span>;
}
</code></pre>
<h3 id="heading-create-the-monnifyclient-class">Create the <code>MonnifyClient</code> Class</h3>
<p>Next, you’ll define the <code>MonnifyClient</code> class. This class encapsulates all communication with the Monnify API. It internally manages API credentials, an Axios HTTP client, an access token, and token expiry tracking.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> MonnifyClient {
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> apiKey: <span class="hljs-built_in">string</span>;
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> secretKey: <span class="hljs-built_in">string</span>;
  <span class="hljs-keyword">private</span> baseUrl: <span class="hljs-built_in">string</span>;
  <span class="hljs-keyword">private</span> contractCode: <span class="hljs-built_in">string</span>;
  <span class="hljs-keyword">private</span> client: AxiosInstance;

  <span class="hljs-keyword">private</span> accessToken: <span class="hljs-built_in">string</span> | <span class="hljs-literal">null</span> = <span class="hljs-literal">null</span>;
  <span class="hljs-keyword">private</span> tokenExpiry: <span class="hljs-built_in">number</span> = <span class="hljs-number">0</span>;
</code></pre>
<p>This design ensures authentication is handled transparently and automatically for every request.</p>
<h3 id="heading-axios-client-and-request-interceptor">Axios Client and Request Interceptor</h3>
<p>Inside the constructor, initialize the Monnify client with credentials from environment variables. The Axios instance is created with the Monnify base URL and JSON headers.</p>
<pre><code class="lang-typescript">  <span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) {
    <span class="hljs-built_in">this</span>.apiKey = process.env.MONNIFY_API_KEY || <span class="hljs-string">''</span>;
    <span class="hljs-built_in">this</span>.secretKey = process.env.MONNIFY_SECRET_KEY || <span class="hljs-string">''</span>;
    <span class="hljs-built_in">this</span>.baseUrl = process.env.MONNIFY_BASE_URL || <span class="hljs-string">'https://api.monnify.com'</span>;
    <span class="hljs-built_in">this</span>.contractCode = process.env.MONNIFY_CONTRACT_CODE || <span class="hljs-string">''</span>;

    <span class="hljs-built_in">this</span>.client = axios.create({
      baseURL: <span class="hljs-built_in">this</span>.baseUrl,
      headers: {
        <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span>,
      },
    });
</code></pre>
<p>We attach the request interceptor to this client to automatically inject a valid Bearer token into every outgoing request (except the authentication endpoint). Before each request, the interceptor ensures the client is authenticated, preventing unauthorized requests and eliminating token-related boilerplate across the codebase.</p>
<pre><code class="lang-typescript">    <span class="hljs-built_in">this</span>.client.interceptors.request.use(<span class="hljs-keyword">async</span> (config: <span class="hljs-built_in">any</span>) =&gt; {
      <span class="hljs-comment">// Skip auth for the login endpoint itself</span>
      <span class="hljs-keyword">if</span> (config.url?.includes(<span class="hljs-string">'/auth/login'</span>)) {
        <span class="hljs-keyword">return</span> config;
      }

      <span class="hljs-comment">// Ensure a valid token exists before every request</span>
      <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.ensureAuthenticated();

      <span class="hljs-keyword">if</span> (<span class="hljs-built_in">this</span>.accessToken) {
        config.headers.Authorization = <span class="hljs-string">`Bearer <span class="hljs-subst">${<span class="hljs-built_in">this</span>.accessToken}</span>`</span>;
      }

      <span class="hljs-keyword">return</span> config;
    });
  }
</code></pre>
<h3 id="heading-authenticate-with-monnify">Authenticate with Monnify</h3>
<p>Authentication is handled using Monnify’s Basic Auth mechanism, where the API key and secret key are base64-encoded and sent to the <code>/auth/login</code> endpoint. Upon successful authentication, the client stores the returned access token and sets an internal expiry timestamp slightly below the official token lifetime to avoid edge-case expirations. Any authentication failure is logged and surfaced as a controlled error to prevent silent failures.</p>
<pre><code class="lang-typescript">
  <span class="hljs-keyword">private</span> <span class="hljs-keyword">async</span> authenticate(): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-comment">// Encode credentials as Base64 for Basic Auth</span>
      <span class="hljs-keyword">const</span> credentials = Buffer.from(
        <span class="hljs-string">`<span class="hljs-subst">${<span class="hljs-built_in">this</span>.apiKey}</span>:<span class="hljs-subst">${<span class="hljs-built_in">this</span>.secretKey}</span>`</span>
      ).toString(<span class="hljs-string">'base64'</span>);

      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> axios.post(
        <span class="hljs-string">`<span class="hljs-subst">${<span class="hljs-built_in">this</span>.baseUrl}</span>/api/v1/auth/login`</span>,
        {},
        {
          headers: {
            Authorization: <span class="hljs-string">`Basic <span class="hljs-subst">${credentials}</span>`</span>,
            <span class="hljs-string">'Content-Type'</span>: <span class="hljs-string">'application/json'</span>,
          },
        }
      );

      <span class="hljs-built_in">this</span>.accessToken = response.data.responseBody.accessToken;
      <span class="hljs-comment">// Set expiry to 23 hours (Monnify tokens typically last 24 hours)</span>
      <span class="hljs-comment">// This prevents edge cases where token expires mid-request</span>
      <span class="hljs-built_in">this</span>.tokenExpiry = <span class="hljs-built_in">Date</span>.now() + <span class="hljs-number">23</span> * <span class="hljs-number">60</span> * <span class="hljs-number">60</span> * <span class="hljs-number">1000</span>;
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      <span class="hljs-built_in">console</span>.error(
        <span class="hljs-string">'Monnify authentication error:'</span>,
        error.response?.data || error.message
      );
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Failed to authenticate with Monnify'</span>);
    }
  }
</code></pre>
<h3 id="heading-automatic-token-refresh-ensureauthenticated">Automatic Token Refresh (<code>ensureAuthenticated</code>)</h3>
<p>Before any API call, the client verifies whether a valid access token exists or if the token has expired. If so, it transparently re-authenticates.</p>
<pre><code class="lang-typescript">  <span class="hljs-keyword">private</span> <span class="hljs-keyword">async</span> ensureAuthenticated(): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
    <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">this</span>.accessToken || <span class="hljs-built_in">Date</span>.now() &gt;= <span class="hljs-built_in">this</span>.tokenExpiry) {
      <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.authenticate();
    }
  }
</code></pre>
<p>This ensures that long-running processes such as payroll queues or background workers can safely make Monnify requests without manual token handling.</p>
<h3 id="heading-initiating-bulk-transfers">Initiating Bulk Transfers</h3>
<p>The <code>initiateBulkTransfer</code> method handles the creation of a bulk disbursement batch, typically used for payroll payments. It validates input transfers to ensure each payment has a valid amount, destination account number, and bank code.</p>
<p>A structured batch request is then constructed, including a unique batch reference, source account (contract code), narration, and a list of transactions. The request is logged for traceability and sent to Monnify’s batch disbursement endpoint. Any API error is normalized and returned with meaningful messaging to aid debugging and retries.</p>
<pre><code class="lang-typescript">  <span class="hljs-keyword">async</span> initiateBulkTransfer(
    transfers: <span class="hljs-built_in">Array</span>&lt;{
      amount: <span class="hljs-built_in">number</span>;
      recipientAccountNumber: <span class="hljs-built_in">string</span>;
      recipientBankCode: <span class="hljs-built_in">string</span>;
      recipientName: <span class="hljs-built_in">string</span>;
      narration: <span class="hljs-built_in">string</span>;
      reference: <span class="hljs-built_in">string</span>;
    }&gt;
  ): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">any</span>&gt; {
    <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.ensureAuthenticated();
</code></pre>
<p>We validate inputs early to fail fast:</p>
<pre><code class="lang-typescript">    <span class="hljs-keyword">if</span> (!transfers || transfers.length === <span class="hljs-number">0</span>) {
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'No transfers provided'</span>);
    }

    <span class="hljs-keyword">if</span> (!<span class="hljs-built_in">this</span>.contractCode) {
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Monnify contract code is not configured'</span>);
    }
</code></pre>
<p>Each transfer is validated individually:</p>
<pre><code class="lang-typescript">    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> transfer <span class="hljs-keyword">of</span> transfers) {
      <span class="hljs-keyword">if</span> (!transfer.amount || transfer.amount &lt;= <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Invalid amount for transfer: <span class="hljs-subst">${transfer.reference}</span>`</span>);
      }
      <span class="hljs-keyword">if</span> (!transfer.recipientAccountNumber) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Missing account number for transfer: <span class="hljs-subst">${transfer.reference}</span>`</span>);
      }
      <span class="hljs-keyword">if</span> (!transfer.recipientBankCode) {
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">`Missing bank code for transfer: <span class="hljs-subst">${transfer.reference}</span>`</span>);
      }
    }
</code></pre>
<p>We then construct the batch payload:</p>
<pre><code class="lang-typescript">    <span class="hljs-keyword">const</span> requestBody = {
      title: <span class="hljs-string">'Bulk Payroll Transfers'</span>,
      batchReference: <span class="hljs-string">`BATCH_<span class="hljs-subst">${<span class="hljs-built_in">Date</span>.now()}</span>`</span>,
      narration: <span class="hljs-string">'Payroll batch disbursement'</span>,
      sourceAccountNumber: <span class="hljs-built_in">this</span>.contractCode,
      onValidationFailure: <span class="hljs-string">'CONTINUE'</span>,
      notificationInterval: <span class="hljs-number">50</span>,
      transactionList: transfers.map(<span class="hljs-function">(<span class="hljs-params">t</span>) =&gt;</span> ({
        amount: t.amount,
        reference: t.reference,
        narration: t.narration,
        destinationBankCode: t.recipientBankCode,
        destinationAccountNumber: t.recipientAccountNumber,
        currency: <span class="hljs-string">'NGN'</span>,
      })),
    };
</code></pre>
<p>Finally, we send the request and normalize errors:</p>
<pre><code class="lang-typescript">    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.client.post(
        <span class="hljs-string">'/api/v2/disbursements/batch'</span>,
        requestBody
      );
      <span class="hljs-keyword">return</span> response.data;
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      <span class="hljs-keyword">const</span> errorData = error.response?.data;
      <span class="hljs-keyword">const</span> message =
        errorData?.responseMessage ||
        errorData?.message ||
        <span class="hljs-string">`Monnify API error (<span class="hljs-subst">${error.response?.status}</span>)`</span>;
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(message);
    }
  }
</code></pre>
<h3 id="heading-authorizing-bulk-transfers-otp-validation">Authorizing Bulk Transfers (OTP Validation)</h3>
<p>Some bulk transfers require OTP authorization. The <code>authorizeBulkTransfer</code> method validates the presence of a batch reference and authorization code before submitting them to Monnify’s OTP validation endpoint. This step finalizes the batch disbursement and allows processing to continue. Errors are logged and surfaced clearly for operational visibility.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> authorizeBulkTransfer(
reference: <span class="hljs-built_in">string</span>,
authorizationCode: <span class="hljs-built_in">string</span>
): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">any</span>&gt; {
<span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.ensureAuthenticated();
    <span class="hljs-keyword">if</span> (!reference) {
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Batch reference is required'</span>);
    }

    <span class="hljs-keyword">if</span> (!authorizationCode) {
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Authorization code (OTP) is required'</span>);
    }

    <span class="hljs-keyword">const</span> requestBody = {
      reference,
      authorizationCode,
    };

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.client.post(
        <span class="hljs-string">'/api/v2/disbursements/batch/validate-otp'</span>,
        requestBody
      );

      <span class="hljs-keyword">return</span> response.data;
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      <span class="hljs-keyword">const</span> errorDetails = error.response?.data || error.message;
      <span class="hljs-built_in">console</span>.error(
        <span class="hljs-string">'Monnify authorization error:'</span>,
        <span class="hljs-built_in">JSON</span>.stringify(errorDetails, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>)
      );

      <span class="hljs-keyword">if</span> (error.response) {
        <span class="hljs-keyword">const</span> errorData = error.response.data;
        <span class="hljs-keyword">const</span> errorMessage =
          errorData?.responseMessage ||
          errorData?.message ||
          <span class="hljs-string">`Monnify API error (<span class="hljs-subst">${error.response.status}</span>)`</span>;
        <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(errorMessage);
      }
      <span class="hljs-keyword">throw</span> error;
    }
}
</code></pre>
<h3 id="heading-transaction-status-lookup">Transaction Status Lookup</h3>
<p>The <code>getTransactionStatus</code> method retrieves the real-time status of an individual transaction using its reference.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> getTransactionStatus(transactionReference: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">any</span>&gt; {
<span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.ensureAuthenticated();
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.client.get(
        <span class="hljs-string">`/api/v2/disbursements/<span class="hljs-subst">${transactionReference}</span>/status`</span>
      );
      <span class="hljs-keyword">return</span> response.data;
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      <span class="hljs-built_in">console</span>.error(
        <span class="hljs-string">'Monnify status check error:'</span>,
        error.response?.data || error.message
      );
      <span class="hljs-keyword">throw</span> error;
    }
}
</code></pre>
<p>This is useful for reconciliation, webhook fallbacks, or manual verification of disbursement outcomes.</p>
<h3 id="heading-batch-details-retrieval">Batch Details Retrieval</h3>
<p>The <code>getBatchDetails</code> method fetches detailed information about an entire disbursement batch, including the state of individual transactions.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> getBatchDetails(batchReference: <span class="hljs-built_in">string</span>): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">any</span>&gt; {
<span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.ensureAuthenticated();
    <span class="hljs-keyword">if</span> (!batchReference) {
      <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Batch reference is required'</span>);
    }

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.client.get(
        <span class="hljs-string">`/api/v2/disbursements/batch/<span class="hljs-subst">${batchReference}</span>`</span>
      );
      <span class="hljs-keyword">return</span> response.data;
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      <span class="hljs-built_in">console</span>.error(
        <span class="hljs-string">'Monnify batch details error:'</span>,
        error.response?.data || error.message
      );
      <span class="hljs-keyword">throw</span> error;
    }
}
</code></pre>
<p>This is particularly useful when reconciling payroll runs or recovering from partial failures.</p>
<h3 id="heading-wallet-balance-check">Wallet Balance Check</h3>
<p>Finally, we can query the available balance of the Monnify wallet.</p>
<p>The <code>getAccountBalance</code> method retrieves the available balance of the configured Monnify wallet (contract account).</p>
<p>Create <code>src/config/monnify.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> getAccountBalance(): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">any</span>&gt; {
<span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.ensureAuthenticated();

    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> <span class="hljs-built_in">this</span>.client.get(
        <span class="hljs-string">`/api/v2/disbursements/wallet-balance?accountNumber=<span class="hljs-subst">${<span class="hljs-built_in">this</span>.contractCode}</span>`</span>
      );
      <span class="hljs-keyword">return</span> response.data;
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      <span class="hljs-built_in">console</span>.error(
        <span class="hljs-string">'Monnify balance check error:'</span>,
        error.response?.data || error.message
      );
      <span class="hljs-keyword">throw</span> error;
    }
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> monnifyClient = <span class="hljs-keyword">new</span> MonnifyClient();
</code></pre>
<p>Key features of this client:</p>
<ol>
<li><p><strong>Automatic token management</strong>: The client automatically handles authentication and refreshes tokens before they expire.</p>
</li>
<li><p><strong>Request interceptor</strong>: Every API request automatically includes the authentication token.</p>
</li>
<li><p><strong>Bulk transfers</strong>: Uses Monnify's batch disbursement API for efficient payroll processing.</p>
</li>
<li><p><strong>Error handling</strong>: Comprehensive error handling with meaningful error messages.</p>
</li>
</ol>
<h2 id="heading-implementing-background-job-processing">Implementing Background Job Processing</h2>
<p>To avoid blocking HTTP requests and to ensure reliable retries, payroll execution is handled asynchronously using a background job processor. This worker is responsible for orchestrating bulk payroll disbursements, coordinating with Monnify, updating payroll and payroll item states, and handling retries safely.</p>
<p>Begin by creating a new file at <code>src/jobs/payroll.processor.ts</code>. All background payroll execution logic will live in this file.</p>
<h3 id="heading-set-up-the-payroll-processing-queue">Set Up the Payroll Processing Queue</h3>
<p>We’ll create a Bull queue named <code>payroll-processing</code> and a backed by Redis. Redis connection details are loaded from environment variables, allowing flexibility across environments.</p>
<p>Default job options are configured to retry failed jobs up to three times using an exponential backoff strategy. This ensures resilience against transient failures such as network issues or temporary payment gateway downtime. Completed jobs are automatically removed from the queue to keep Redis storage clean.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> Queue <span class="hljs-keyword">from</span> <span class="hljs-string">'bull'</span>;
<span class="hljs-keyword">import</span> { monnifyClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'../config/monnify'</span>;
<span class="hljs-keyword">import</span> {
 PayrollItemModel,
 PayrollModel,
 PayrollStatus,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'../models/payroll'</span>;
<span class="hljs-keyword">import</span> { EmployeeModel } <span class="hljs-keyword">from</span> <span class="hljs-string">'../models/employee'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> payrollQueue = <span class="hljs-keyword">new</span> Queue(<span class="hljs-string">'payroll-processing'</span>, {
 redis: {
 host: process.env.REDIS_HOST || <span class="hljs-string">'localhost'</span>,
 port: <span class="hljs-built_in">Number</span>(process.env.REDIS_PORT || <span class="hljs-number">6379</span>),
},
defaultJobOptions: {
 attempts: <span class="hljs-number">3</span>,
 backoff: { 
  <span class="hljs-keyword">type</span>: <span class="hljs-string">'exponential'</span>, 
  delay: <span class="hljs-number">2000</span> 
},
 removeOnComplete: <span class="hljs-literal">true</span>,
},
});
</code></pre>
<h3 id="heading-queue-processor-registration">Queue Processor Registration</h3>
<p>The queue registers a processor function using <code>payrollQueue.process</code>, which receives jobs containing a <code>payrollId</code>. Each job triggers the <code>processBulkPayroll</code> function, making the queue responsible for executing one payroll batch at a time.</p>
<pre><code class="lang-typescript">payrollQueue.process(<span class="hljs-keyword">async</span> (job) =&gt; {
 <span class="hljs-keyword">return</span> processBulkPayroll(job.data.payrollId);
});
</code></pre>
<p>This design decouples payroll execution from HTTP requests and allows processing to happen asynchronously in background workers.</p>
<h3 id="heading-bulk-payroll-processing-flow-processbulkpayroll">Bulk Payroll Processing Flow (<code>processBulkPayroll</code>)</h3>
<p>When a payroll job is picked up, the system first fetches all payroll items associated with the given payroll ID. It filters out only items that are eligible for processing: those still in a <code>PENDING</code> state or previously marked as <code>PROCESSING</code> but missing a transaction reference.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">processBulkPayroll</span>(<span class="hljs-params">payrollId: <span class="hljs-built_in">number</span></span>) </span>{

<span class="hljs-keyword">const</span> items = <span class="hljs-keyword">await</span> PayrollItemModel.findByPayrollId(payrollId);
</code></pre>
<p>Also, it filters payroll items to include only those that still require processing. This prevents duplicate payments when jobs are retried.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> payable = items.filter(
  <span class="hljs-function">(<span class="hljs-params">i</span>) =&gt;</span>
    i.status === PayrollStatus.PENDING ||
    (i.status === PayrollStatus.PROCESSING &amp;&amp; !i.transaction_reference)
);

<span class="hljs-keyword">if</span> (payable.length === <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span>;
</code></pre>
<p>If no payable items remain, the function exits early, avoiding unnecessary API calls.</p>
<p>Once we confirm there are payable items, we update the overall payroll status:</p>
<pre><code class="lang-typescript">  <span class="hljs-keyword">await</span> PayrollModel.updateStatus(payrollId, PayrollStatus.PROCESSING);
</code></pre>
<p>This provides immediate visibility that disbursement is underway.</p>
<h3 id="heading-building-the-bulk-transfer-payload">Building the Bulk Transfer Payload</h3>
<p>Create a variable to store the transfer list that will be sent to Monnify.</p>
<pre><code class="lang-typescript">  <span class="hljs-keyword">const</span> transfers = [];
</code></pre>
<p>For each payable payroll item, the corresponding employee record is fetched to retrieve bank and account details. A unique payment reference is generated using the payroll ID and payroll item ID, ensuring traceability across systems. Each payroll item is immediately marked as <code>PROCESSING</code> before initiating payment to prevent concurrent workers from attempting to process the same item.</p>
<p>A transfer object is then constructed containing the payment amount, recipient bank details, narration, and unique reference. These transfer objects are accumulated into a single batch request.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> item <span class="hljs-keyword">of</span> payable) {
<span class="hljs-keyword">const</span> employee = <span class="hljs-keyword">await</span> EmployeeModel.findById(item.employee_id);
<span class="hljs-keyword">if</span> (!employee) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Employee not found'</span>);

    <span class="hljs-keyword">const</span> reference = <span class="hljs-string">`PAYROLL_<span class="hljs-subst">${payrollId}</span>_<span class="hljs-subst">${item.id}</span>`</span>;

    <span class="hljs-keyword">await</span> PayrollItemModel.updateStatus(item.id, PayrollStatus.PROCESSING);

    transfers.push({
      amount: <span class="hljs-built_in">Number</span>(item.amount),
      reference,
      recipientAccountNumber: employee.account_number,
      recipientBankCode: employee.bank_code,
      recipientName: employee.name,
      narration: <span class="hljs-string">`Payroll payment`</span>,
    });

}
</code></pre>
<h3 id="heading-initiating-bulk-disbursement-via-monnify">Initiating Bulk Disbursement via Monnify</h3>
<p>Once all transfers are prepared, the system initiates a bulk transfer through the Monnify client.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> monnifyClient.initiateBulkTransfer(transfers);

<span class="hljs-keyword">if</span> (!response?.requestSuccessful) {
  <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>(<span class="hljs-string">'Bulk transfer initiation failed'</span>);
}
</code></pre>
<p>If Monnify doesn’t confirm successful initiation, the job throws an error, allowing Bull’s retry mechanism to take over. This ensures failed initiation attempts are retried safely without manual intervention.</p>
<h3 id="heading-storing-transaction-references">Storing Transaction References</h3>
<p>After a successful bulk transfer initiation, Monnify returns a list of transactions containing unique transaction references. The system matches each response entry to its corresponding payroll item using the generated reference and updates the payroll item record with the Monnify transaction reference while keeping its status as <code>PROCESSING</code>.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> results = response.responseBody?.transactionList || [];

<span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> item <span class="hljs-keyword">of</span> payable) {
<span class="hljs-keyword">const</span> ref = <span class="hljs-string">`PAYROLL_<span class="hljs-subst">${payrollId}</span>_<span class="hljs-subst">${item.id}</span>`</span>;
<span class="hljs-keyword">const</span> match = results.find(<span class="hljs-function">(<span class="hljs-params">r: <span class="hljs-built_in">any</span></span>) =&gt;</span> r.reference === ref);

    <span class="hljs-keyword">if</span> (match?.transactionReference) {
      <span class="hljs-keyword">await</span> PayrollItemModel.updateStatus(
        item.id,
        PayrollStatus.PROCESSING,
        match.transactionReference
      );
    }

}

<span class="hljs-keyword">await</span> updatePayrollStats(payrollId);
}
</code></pre>
<p>This step is critical for later reconciliation through webhooks or status polling.</p>
<h3 id="heading-payroll-statistics-reconciliation-updatepayrollstats">Payroll Statistics Reconciliation (<code>updatePayrollStats</code>)</h3>
<p>After initiating payments, the system recalculates payroll-level statistics by refetching all payroll items.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">updatePayrollStats</span>(<span class="hljs-params">payrollId: <span class="hljs-built_in">number</span></span>) </span>{
<span class="hljs-keyword">const</span> items = <span class="hljs-keyword">await</span> PayrollItemModel.findByPayrollId(payrollId);

<span class="hljs-keyword">const</span> completed = items.filter(
  <span class="hljs-function">(<span class="hljs-params">i</span>) =&gt;</span> i.status === PayrollStatus.COMPLETED
).length;
</code></pre>
<p>The overall payroll status is derived from these counts:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">const</span> failed = items.filter(<span class="hljs-function">(<span class="hljs-params">i</span>) =&gt;</span> i.status === PayrollStatus.FAILED).length;

<span class="hljs-keyword">let</span> status = PayrollStatus.PROCESSING;

<span class="hljs-keyword">if</span> (completed === items.length) {
  status = PayrollStatus.COMPLETED;
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (failed === items.length) {
  status = PayrollStatus.FAILED;
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (completed &gt; <span class="hljs-number">0</span>) {
  status = PayrollStatus.PARTIALLY_COMPLETED;
}

 <span class="hljs-keyword">await</span> PayrollModel.updateStatus(payrollId, status, completed, failed);
}
</code></pre>
<p>If all items are completed, the payroll is marked as <code>COMPLETED</code>. If all failed, it’s marked as <code>FAILED</code>. If some succeeded and some failed, it’s marked as <code>PARTIALLY_COMPLETED</code>. Otherwise, it remains in <code>PROCESSING</code>. The payroll record is then updated with the new status and aggregate counts, providing an accurate real-time snapshot of payroll execution.</p>
<h3 id="heading-queue-entry-point-processpayrollitems">Queue Entry Point (<code>processPayrollItems</code>)</h3>
<p>The <code>processPayrollItems</code> function serves as the public entry point for triggering payroll execution.</p>
<pre><code class="lang-typescript"><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">processPayrollItems</span>(<span class="hljs-params">payrollId: <span class="hljs-built_in">number</span></span>) </span>{
  <span class="hljs-keyword">await</span> payrollQueue.add({ payrollId, <span class="hljs-keyword">type</span>: <span class="hljs-string">'bulk'</span> });
}
</code></pre>
<p>It simply enqueues a payroll job with the relevant payroll ID, allowing controllers or services to initiate payroll processing without coupling themselves to queue logic or payment execution details.</p>
<h3 id="heading-role-in-the-overall-payroll-architecture">Role in the Overall Payroll Architecture</h3>
<p>This queue worker acts as the execution engine of the payroll system. It:</p>
<ul>
<li><p>Bridges payroll domain models with the Monnify payment gateway</p>
</li>
<li><p>Ensures safe retries through Bull’s job management and maintains idempotency</p>
</li>
<li><p>Continuously synchronizes payroll and payroll item states</p>
</li>
</ul>
<p>By offloading payment execution to background workers, the system achieves scalability, reliability, and operational resilience required for real-world payroll processing.</p>
<p>Key features of the job processor:</p>
<ol>
<li><p><strong>Exponential backoff</strong>: Failed jobs are retried with increasing delays (2s, 4s, 8s).</p>
</li>
<li><p><strong>Bulk processing</strong>: All payroll items are processed as a single batch transfer.</p>
</li>
<li><p><strong>Status tracking</strong>: Each item's status is updated throughout the process.</p>
</li>
<li><p><strong>Automatic cleanup</strong>: Completed jobs are automatically removed from the queue.</p>
</li>
</ol>
<h2 id="heading-creating-the-api-controllers">Creating the API Controllers</h2>
<p>Next, we’ll build the HTTP controller layer for managing employees in the payroll system using Express.js. It exposes RESTful API endpoints that handle incoming requests, perform validation, interact with the employee data model, and return appropriate HTTP responses.</p>
<p>The controller acts as the bridge between client-facing APIs and the underlying business logic encapsulated in the <code>EmployeeModel</code>.</p>
<h3 id="heading-controller-responsibilities">Controller Responsibilities</h3>
<p>The <code>EmployeeController</code> is responsible for:</p>
<ul>
<li><p>Validating incoming request data</p>
</li>
<li><p>Calling the appropriate model methods</p>
</li>
<li><p>Handling errors gracefully</p>
</li>
<li><p>Returning meaningful HTTP status codes and JSON responses</p>
</li>
</ul>
<p>Each method follows a consistent structure using <code>try–catch</code> blocks to ensure reliability and simplify error handling.</p>
<p>Start by creating a new file at <code>src/controllers/employee.controller.ts</code>. This file will contain all the endpoints needed to manage employees in the payroll system.</p>
<p>At the top of the file, import the required Express types and the employee model:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Request, Response } <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> { EmployeeModel, CreateEmployeeInput } <span class="hljs-keyword">from</span> <span class="hljs-string">'../models/employee'</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> EmployeeController {
  <span class="hljs-comment">// Controller methods will go here</span>
}
</code></pre>
<p>Each method inside this class will map to a specific API endpoint.</p>
<h3 id="heading-creating-an-employee-createemployee">Creating an Employee (<code>createEmployee</code>)</h3>
<p>We’ll start with an endpoint for creating a new employee.</p>
<p>This endpoint handles the creation of a new employee record. It extracts the request body and validates the presence of required fields such as name, email, salary, bank account number, and bank code.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> createEmployee(req: Request, res: Response): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> data: CreateEmployeeInput = req.body;

      <span class="hljs-keyword">if</span> (
        !data.name ||
        !data.email ||
        !data.salary ||
        !data.account_number ||
        !data.bank_code
      ) {
        res.status(<span class="hljs-number">400</span>).json({
          error:
            <span class="hljs-string">'Missing required fields: name, email, salary, account_number, bank_code'</span>,
        });
        <span class="hljs-keyword">return</span>;
      }

      <span class="hljs-keyword">const</span> employee = <span class="hljs-keyword">await</span> EmployeeModel.create(data);
      res.status(<span class="hljs-number">201</span>).json({
        message: <span class="hljs-string">'Employee created successfully'</span>,
        data: employee,
      });
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error creating employee:'</span>, error);
      res
        .status(<span class="hljs-number">500</span>)
        .json({ error: error.message || <span class="hljs-string">'Failed to create employee'</span> });
    }

}
</code></pre>
<p>If any required field is missing, the request is rejected with a <code>400 Bad Request</code> response.</p>
<p>Upon successful validation, the controller delegates employee creation to the <code>EmployeeModel.create</code> method and returns a <code>201 Created</code> response containing the newly created employee. Any unexpected error during the process results in a <code>500 Internal Server Error</code>.</p>
<h3 id="heading-fetching-all-employees-getallemployees">Fetching All Employees (<code>getAllEmployees</code>)</h3>
<p>Next, we’ll add an endpoint for retrieving all employee records from the system.</p>
<p>This endpoint simply calls <code>EmployeeModel.findAll</code> and returns the result as a JSON response. This API is typically used for administrative dashboards, payroll preparation, or reporting purposes.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> getAllEmployees(req: Request, res: Response): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">const</span> employees = <span class="hljs-keyword">await</span> EmployeeModel.findAll();
    res.json({ data: employees });
  } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
    <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error fetching employees:'</span>, error);
    res
      .status(<span class="hljs-number">500</span>)
      .json({ error: error.message || <span class="hljs-string">'Failed to fetch employees'</span> });
  }
}
</code></pre>
<p>If the retrieval is successful, the controller responds with the full list of employees. If something goes wrong, such as a database or unexpected runtime error, the error is logged and a 500 Internal Server Error is returned to the client.</p>
<h3 id="heading-fetching-a-single-employee-getemployeebyid">Fetching a Single Employee (<code>getEmployeeById</code>)</h3>
<p>After listing all employees, the next logical step is being able to fetch a single employee by their ID.</p>
<p>This endpoint retrieves a specific employee by ID, which is parsed from the URL parameters. If the employee doesn’t exist, the controller responds with a <code>404 Not Found</code>. Otherwise, the employee data is returned in a successful response. This endpoint is useful for viewing or editing individual employee details.</p>
<pre><code class="lang-typescript">  <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> getEmployeeById(req: Request, res: Response): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> { id } = req.params;
      <span class="hljs-keyword">const</span> employee = <span class="hljs-keyword">await</span> EmployeeModel.findById(<span class="hljs-built_in">parseInt</span>(id));

      <span class="hljs-keyword">if</span> (!employee) {
        res.status(<span class="hljs-number">404</span>).json({ error: <span class="hljs-string">'Employee not found'</span> });
        <span class="hljs-keyword">return</span>;
      }

      res.json({ data: employee });
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error fetching employee:'</span>, error);
      res
        .status(<span class="hljs-number">500</span>)
        .json({ error: error.message || <span class="hljs-string">'Failed to fetch employee'</span> });
    }
  }
</code></pre>
<h3 id="heading-updating-an-employee-updateemployee">Updating an Employee (<code>updateEmployee</code>)</h3>
<p>Now that we can retrieve individual employees, the next step is allowing their details to be updated.</p>
<p>This endpoint allows partial updates to an existing employee record. It first checks whether the employee exists before attempting an update.</p>
<p>If the employee isn’t found, a <code>404 Not Found</code> response is returned. If the employee exists, the controller forwards the update payload to <code>EmployeeModel.update</code> and returns the updated employee record. This approach ensures data integrity and prevents silent failures.</p>
<pre><code class="lang-typescript">  <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> updateEmployee(req: Request, res: Response): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> { id } = req.params;
      <span class="hljs-keyword">const</span> data: Partial&lt;CreateEmployeeInput&gt; = req.body;

      <span class="hljs-keyword">const</span> employee = <span class="hljs-keyword">await</span> EmployeeModel.findById(<span class="hljs-built_in">parseInt</span>(id));
      <span class="hljs-keyword">if</span> (!employee) {
        res.status(<span class="hljs-number">404</span>).json({ error: <span class="hljs-string">'Employee not found'</span> });
        <span class="hljs-keyword">return</span>;
      }

      <span class="hljs-keyword">const</span> updated = <span class="hljs-keyword">await</span> EmployeeModel.update(<span class="hljs-built_in">parseInt</span>(id), data);
      res.json({
        message: <span class="hljs-string">'Employee updated successfully'</span>,
        data: updated,
      });
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error updating employee:'</span>, error);
      res
        .status(<span class="hljs-number">500</span>)
        .json({ error: error.message || <span class="hljs-string">'Failed to update employee'</span> });
    }
  }
</code></pre>
<h3 id="heading-deleting-an-employee-deleteemployee">Deleting an Employee (<code>deleteEmployee</code>)</h3>
<p>Finally, the last endpoint in the <code>EmployeeController</code> handles employee deletion.</p>
<p>Before deleting, it verifies that the employee exists to avoid invalid delete operations. If found, the employee record is removed using <code>EmployeeModel.delete</code>, and a success message is returned. If the employee doesn’t exist, the controller responds with a <code>404 Not Found</code>.</p>
<pre><code class="lang-typescript"> <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> deleteEmployee(req: Request, res: Response): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> { id } = req.params;

      <span class="hljs-keyword">const</span> employee = <span class="hljs-keyword">await</span> EmployeeModel.findById(<span class="hljs-built_in">parseInt</span>(id));
      <span class="hljs-keyword">if</span> (!employee) {
        res.status(<span class="hljs-number">404</span>).json({ error: <span class="hljs-string">'Employee not found'</span> });
        <span class="hljs-keyword">return</span>;
      }

      <span class="hljs-keyword">await</span> EmployeeModel.delete(<span class="hljs-built_in">parseInt</span>(id));
      res.json({ message: <span class="hljs-string">'Employee deleted successfully'</span> });
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error deleting employee:'</span>, error);
      res
        .status(<span class="hljs-number">500</span>)
        .json({ error: error.message || <span class="hljs-string">'Failed to delete employee'</span> });
    }
  }
</code></pre>
<h3 id="heading-error-handling-strategy">Error Handling Strategy</h3>
<p>All controller methods use structured error handling to log errors internally while returning clean and user-friendly error messages to API consumers. This separation ensures sensitive implementation details are not leaked while still providing useful feedback for debugging and client-side handling.</p>
<h3 id="heading-role-in-the-overall-payroll-system">Role in the Overall Payroll System</h3>
<p>The <code>EmployeeController</code> provides the foundational APIs required for managing employee records, which are essential inputs for payroll processing. By cleanly separating HTTP concerns from business logic and persistence layers, this controller supports maintainability, scalability, and clear system boundaries within the payroll architecture.</p>
<h3 id="heading-payroll-controller">Payroll Controller</h3>
<p>This module defines the PayrollController, which serves as the primary HTTP-facing orchestration layer for payroll operations in the system. It exposes RESTful APIs that allow clients to create payrolls, retrieve payroll data, trigger payroll processing, reconcile payment results, authorize bulk transfers, and monitor transaction and account statuses.</p>
<h3 id="heading-controller-responsibilities-1">Controller Responsibilities</h3>
<p>The <code>PayrollController</code> is responsible for:</p>
<ul>
<li><p>Accepting and validating client requests related to payrolls</p>
</li>
<li><p>Managing payroll lifecycle transitions (creation → processing → completion)</p>
</li>
<li><p>Triggering background job execution for bulk payroll disbursement</p>
</li>
<li><p>Reconciling payment results with Monnify</p>
</li>
<li><p>Providing real-time payroll and transaction status visibility</p>
</li>
<li><p>Acting as a safe boundary between external clients and internal services</p>
</li>
</ul>
<p>To get started, create a new file <code>src/controllers/payroll.controller.ts</code>. This is where we’ll define all payroll-related endpoints.</p>
<p>At the top of <code>src/controllers/payroll.controller.ts</code>, we start with the following imports:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Request, Response } <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> {
  PayrollModel,
  PayrollItemModel,
  PayrollStatus,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'../models/payroll'</span>;
<span class="hljs-keyword">import</span> { processPayrollItems } <span class="hljs-keyword">from</span> <span class="hljs-string">'../jobs/payroll.processor'</span>;
<span class="hljs-keyword">import</span> { monnifyClient } <span class="hljs-keyword">from</span> <span class="hljs-string">'../config/monnify'</span>;
</code></pre>
<p>Here’s what each of these is responsible for:</p>
<ul>
<li><p><code>Request</code> and <code>Response</code> (from Express): These types give us strongly typed access to incoming HTTP requests and outgoing responses.</p>
</li>
<li><p><code>PayrollModel</code>: This model handles payroll batch operations such as creating payrolls, fetching them, and updating their overall status.</p>
</li>
<li><p><code>PayrollItemModel</code>: This model lets us fetch and update those items, especially during processing and reconciliation.</p>
</li>
<li><p><code>PayrollStatus</code>: This is an enum that defines the valid states of a payroll or payroll item (for example: <code>PENDING</code>, <code>PROCESSING</code>, <code>COMPLETED</code>, <code>FAILED</code>). Using an enum helps keep state transitions explicit and consistent across the system.</p>
</li>
<li><p><code>processPayrollItems</code>: This function is responsible for handing off payroll processing to background workers. Instead of processing payrolls synchronously in the HTTP request, we queue the work and let workers handle it asynchronously.</p>
</li>
<li><p><code>monnifyClient</code>: This is our gateway to the external payment service. We use it to authorize bulk transfers, check transaction statuses, reconcile payments, and fetch account balances.</p>
</li>
</ul>
<p>Together, these imports give the controller everything it needs to process payroll operations.</p>
<p>With our imports in place, we can now define the controller class itself. This class will serve as the single home for all payroll-related endpoints.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">export</span> <span class="hljs-keyword">class</span> PayrollController {
  <span class="hljs-comment">// Payroll endpoints will live here</span>
}
</code></pre>
<h3 id="heading-creating-a-payroll-createpayroll">Creating a Payroll (<code>createPayroll</code>)</h3>
<p>With the controller in place, we’ll begin by implementing the endpoint create payroll. This endpoint initializes a new payroll batch, allowing us to either process all employees or a subset by their IDs.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> createPayroll(req: Request, res: Response): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> { payroll_period, employee_ids } = req.body;

      <span class="hljs-keyword">if</span> (!payroll_period) {
        res.status(<span class="hljs-number">400</span>).json({ error: <span class="hljs-string">'payroll_period is required'</span> });
        <span class="hljs-keyword">return</span>;
      }

      <span class="hljs-keyword">const</span> processedEmployeeIds = employee_ids
        ? employee_ids
            .map(<span class="hljs-function">(<span class="hljs-params">id: <span class="hljs-built_in">any</span></span>) =&gt;</span> <span class="hljs-built_in">parseInt</span>(id, <span class="hljs-number">10</span>))
            .filter(<span class="hljs-function">(<span class="hljs-params">id: <span class="hljs-built_in">number</span></span>) =&gt;</span> !<span class="hljs-built_in">isNaN</span>(id))
        : <span class="hljs-literal">undefined</span>;

      <span class="hljs-keyword">const</span> payroll = <span class="hljs-keyword">await</span> PayrollModel.create({
        payroll_period,
        employee_ids: processedEmployeeIds,
      });

      res.status(<span class="hljs-number">201</span>).json({
        message: <span class="hljs-string">'Payroll created successfully'</span>,
        data: payroll,
      });
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error creating payroll:'</span>, error);
      res
        .status(<span class="hljs-number">500</span>)
        .json({ error: error.message || <span class="hljs-string">'Failed to create payroll'</span> });
    }

}
</code></pre>
<p>Here’s what’s happening in the code:</p>
<ul>
<li><p>The endpoint requires a <code>payroll_period</code> and optionally accepts a list of employee IDs to support partial payroll runs.</p>
</li>
<li><p>Incoming employee IDs are normalized and validated to ensure they are valid integers before being passed to the payroll model.</p>
</li>
<li><p>The controller delegates the actual creation logic to <code>PayrollModel.create</code>, which computes totals and creates payroll items.</p>
</li>
<li><p>On success, the API responds with a <code>201 Created</code> status and the newly created payroll record.</p>
</li>
</ul>
<h3 id="heading-fetching-all-payrolls-getallpayrolls">Fetching All Payrolls (<code>getAllPayrolls</code>)</h3>
<p>This endpoint retrieves all payroll batches in the system. It’s typically used for administrative dashboards and payroll history views. The controller simply delegates to <code>PayrollModel.findAll</code> and returns the results in a structured JSON response.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> getAllPayrolls(req: Request, res: Response): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> payrolls = <span class="hljs-keyword">await</span> PayrollModel.findAll();
res.json({ data: payrolls });
} <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
<span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error fetching payrolls:'</span>, error);
res
.status(<span class="hljs-number">500</span>)
.json({ error: error.message || <span class="hljs-string">'Failed to fetch payrolls'</span> });
}
}

<span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> getPayrollById(req: Request, res: Response): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> { id } = req.params;
<span class="hljs-keyword">const</span> payroll = <span class="hljs-keyword">await</span> PayrollModel.findById(<span class="hljs-built_in">parseInt</span>(id));

      <span class="hljs-keyword">if</span> (!payroll) {
        res.status(<span class="hljs-number">404</span>).json({ error: <span class="hljs-string">'Payroll not found'</span> });
        <span class="hljs-keyword">return</span>;
      }

      <span class="hljs-keyword">const</span> items = <span class="hljs-keyword">await</span> PayrollItemModel.findByPayrollId(payroll.id);

      res.json({
        data: {
          ...payroll,
          items,
        },
      });
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error fetching payroll:'</span>, error);
      res
        .status(<span class="hljs-number">500</span>)
        .json({ error: error.message || <span class="hljs-string">'Failed to fetch payroll'</span> });
    }
}
</code></pre>
<h3 id="heading-fetching-a-payroll-with-items-getpayrollbyid">Fetching a Payroll with Items (<code>getPayrollById</code>)</h3>
<p>Next, we’ll implement an endpoint to retrieve a single payroll by its ID along with all associated payroll items. This is useful for administrative dashboards and payroll history views.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> getPayrollById(req: Request, res: Response): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> { id } = req.params;
<span class="hljs-keyword">const</span> payroll = <span class="hljs-keyword">await</span> PayrollModel.findById(<span class="hljs-built_in">parseInt</span>(id));

      <span class="hljs-keyword">if</span> (!payroll) {
        res.status(<span class="hljs-number">404</span>).json({ error: <span class="hljs-string">'Payroll not found'</span> });
        <span class="hljs-keyword">return</span>;
      }

      <span class="hljs-keyword">const</span> items = <span class="hljs-keyword">await</span> PayrollItemModel.findByPayrollId(payroll.id);

      res.json({
        data: {
          ...payroll,
          items,
        },
      });
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error fetching payroll:'</span>, error);
      res
        .status(<span class="hljs-number">500</span>)
        .json({ error: error.message || <span class="hljs-string">'Failed to fetch payroll'</span> });
    }

}
</code></pre>
<p>In the code, we read the <code>id</code> parameter from the URL and convert it to an integer.</p>
<p>If the payroll does not exist, a <code>404 Not Found</code> response is returned. When found, the controller aggregates payroll metadata and its child payroll items into a single response object, making it convenient for detailed payroll inspection and UI rendering.</p>
<h3 id="heading-processing-a-payroll-processpayroll">Processing a Payroll (<code>processPayroll</code>)</h3>
<p>Next, we implement the <code>processPayroll</code> endpoint. This endpoint initiates payroll execution. Before queuing the payroll for processing, the controller enforces important state checks to prevent duplicate or invalid execution, ensuring payrolls that are already <code>PROCESSING</code> or <code>COMPLETED</code> cannot be reprocessed.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> processPayroll(req: Request, res: Response): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> { id } = req.params;

      <span class="hljs-keyword">const</span> payroll = <span class="hljs-keyword">await</span> PayrollModel.findById(<span class="hljs-built_in">Number</span>(id));

      <span class="hljs-keyword">if</span> (!payroll) {
        res.status(<span class="hljs-number">404</span>).json({ error: <span class="hljs-string">'Payroll not found'</span> });
        <span class="hljs-keyword">return</span>;
      }

      <span class="hljs-keyword">if</span> (
        payroll.status === PayrollStatus.COMPLETED ||
        payroll.status === PayrollStatus.PROCESSING
      ) {
        res.status(<span class="hljs-number">400</span>).json({
          error: <span class="hljs-string">`Payroll already <span class="hljs-subst">${payroll.status}</span>`</span>,
        });
        <span class="hljs-keyword">return</span>;
      }

      <span class="hljs-comment">// Queue the payroll for processing</span>
      <span class="hljs-keyword">await</span> processPayrollItems(payroll.id);

      res.json({
        message: <span class="hljs-string">'Payroll queued for bulk processing'</span>,
        data: {
          payroll_id: payroll.id,
          processing_mode: <span class="hljs-string">'bulk'</span>,
        },
      });
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error processing payroll:'</span>, error);
      res.status(<span class="hljs-number">500</span>).json({
        error: error.message || <span class="hljs-string">'Failed to process payroll'</span>,
      });
    }
}
</code></pre>
<p>Here’s what’s happening in the code:</p>
<ul>
<li><p>We get the <code>id</code> parameter from the URL and convert it to a number.</p>
</li>
<li><p>If no payroll is found with the given ID, we return a <code>404 Not Found</code> response.</p>
</li>
<li><p>Before queuing, we check the payroll’s current status. Payrolls that are already <code>PROCESSING</code> or <code>COMPLETED</code> cannot be reprocessed.</p>
</li>
<li><p>Valid payrolls are handed off to <code>processPayrollItems</code>, which runs the bulk execution in background workers (Bull jobs).</p>
</li>
<li><p>Once queued, we respond with a JSON object confirming the payroll is ready for bulk processing.</p>
</li>
</ul>
<h3 id="heading-reconciling-payroll-payments-reconcilepayroll">Reconciling Payroll Payments (<code>reconcilePayroll</code>)</h3>
<p>Next, we’ll implement the endpoint that reconciles payroll payments. This ensures that the statuses of payroll items in our system match the actual payment outcomes from Monnify.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> reconcilePayroll(req: Request, res: Response): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> { id } = req.params;

      <span class="hljs-keyword">const</span> payroll = <span class="hljs-keyword">await</span> PayrollModel.findById(<span class="hljs-built_in">Number</span>(id));
      <span class="hljs-keyword">if</span> (!payroll) {
        res.status(<span class="hljs-number">404</span>).json({ error: <span class="hljs-string">'Payroll not found'</span> });
        <span class="hljs-keyword">return</span>;
      }

      <span class="hljs-keyword">const</span> items = <span class="hljs-keyword">await</span> PayrollItemModel.findByPayrollId(<span class="hljs-built_in">Number</span>(id));

      <span class="hljs-keyword">const</span> itemsToReconcile = items.filter(
        <span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> item.transaction_reference
      );

      <span class="hljs-keyword">if</span> (itemsToReconcile.length === <span class="hljs-number">0</span>) {
        res.json({
          message: <span class="hljs-string">'No items to reconcile (no transaction references found)'</span>,
          reconciled: <span class="hljs-number">0</span>,
        });
        <span class="hljs-keyword">return</span>;
      }

      <span class="hljs-keyword">let</span> updated = <span class="hljs-number">0</span>;
      <span class="hljs-keyword">let</span> errors = <span class="hljs-number">0</span>;

      <span class="hljs-keyword">for</span> (<span class="hljs-keyword">const</span> item <span class="hljs-keyword">of</span> itemsToReconcile) {
        <span class="hljs-keyword">try</span> {
          <span class="hljs-keyword">const</span> txStatus = <span class="hljs-keyword">await</span> monnifyClient.getTransactionStatus(
            item.transaction_reference!
          );

          <span class="hljs-keyword">const</span> responseBody = txStatus.responseBody || txStatus;
          <span class="hljs-keyword">const</span> paymentStatus =
            responseBody.paymentStatus || responseBody.status;

          <span class="hljs-keyword">if</span> (
            paymentStatus === <span class="hljs-string">'PAID'</span> &amp;&amp;
            item.status !== PayrollStatus.COMPLETED
          ) {
            <span class="hljs-keyword">await</span> PayrollItemModel.updateStatus(
              item.id,
              PayrollStatus.COMPLETED,
              item.transaction_reference
            );
            updated++;
          } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (
            paymentStatus === <span class="hljs-string">'FAILED'</span> &amp;&amp;
            item.status !== PayrollStatus.FAILED
          ) {
            <span class="hljs-keyword">const</span> errorMessage =
              responseBody.paymentDescription ||
              responseBody.failureReason ||
              <span class="hljs-string">'Transaction failed'</span>;
            <span class="hljs-keyword">await</span> PayrollItemModel.updateStatus(
              item.id,
              PayrollStatus.FAILED,
              item.transaction_reference,
              errorMessage
            );
            updated++;
          }
        } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
          errors++;
          <span class="hljs-built_in">console</span>.error(<span class="hljs-string">`Error reconciling item <span class="hljs-subst">${item.id}</span>:`</span>, error.message);
        }
      }

      <span class="hljs-comment">// Update payroll stats</span>
      <span class="hljs-keyword">await</span> PayrollController.updatePayrollStats(<span class="hljs-built_in">Number</span>(id));

      res.json({
        message: <span class="hljs-string">'Payroll reconciled successfully'</span>,
        reconciled: updated,
        errors,
        total: itemsToReconcile.length,
      });
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error reconciling payroll:'</span>, error);
      res.status(<span class="hljs-number">500</span>).json({
        error: error.message || <span class="hljs-string">'Failed to reconcile payroll'</span>,
      });
    }
}
</code></pre>
<p>The endpoint retrieves all payroll items with transaction references and queries Monnify for each transaction’s status. Based on the response, payroll items are updated to either <code>COMPLETED</code> or <code>FAILED</code>, with failure reasons captured where applicable.</p>
<p>Errors during reconciliation are tracked and logged without aborting the entire reconciliation process. After reconciliation, payroll-level statistics are recalculated to ensure consistency between item-level and batch-level states.</p>
<h3 id="heading-payroll-statistics-update-internal-helper">Payroll Statistics Update (Internal Helper)</h3>
<p>The private <code>updatePayrollStats</code> method recalculates payroll status based on the aggregate states of its payroll items.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> updatePayrollStats(payrollId: <span class="hljs-built_in">number</span>): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
<span class="hljs-keyword">const</span> items = <span class="hljs-keyword">await</span> PayrollItemModel.findByPayrollId(payrollId);

    <span class="hljs-keyword">const</span> completed = items.filter(
      <span class="hljs-function">(<span class="hljs-params">i</span>) =&gt;</span> i.status === PayrollStatus.COMPLETED
    ).length;
    <span class="hljs-keyword">const</span> failed = items.filter(
      <span class="hljs-function">(<span class="hljs-params">i</span>) =&gt;</span> i.status === PayrollStatus.FAILED
    ).length;
    <span class="hljs-keyword">const</span> total = items.length;

    <span class="hljs-keyword">let</span> status: PayrollStatus;
    <span class="hljs-keyword">if</span> (completed === total) {
      status = PayrollStatus.COMPLETED;
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (failed === total) {
      status = PayrollStatus.FAILED;
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (completed &gt; <span class="hljs-number">0</span>) {
      status = PayrollStatus.PARTIALLY_COMPLETED;
    } <span class="hljs-keyword">else</span> {
      status = PayrollStatus.PROCESSING;
    }

    <span class="hljs-keyword">await</span> PayrollModel.updateStatus(payrollId, status, completed, failed);

}
</code></pre>
<p>The endpoint determines whether a payroll is fully completed, fully failed, partially completed, or still processing, and updates the payroll record accordingly.</p>
<p>This logic guarantees that the payroll’s summary status always reflects the true execution state of its underlying payments.</p>
<h3 id="heading-fetching-payroll-status-summary-getpayrollstatus">Fetching Payroll Status Summary (<code>getPayrollStatus</code>)</h3>
<p>Next, we’ll implement the <code>getPayrollStatus</code> endpoint. This endpoint provides a comprehensive status snapshot of a payroll. In addition to returning payroll metadata and items, it computes a summary breakdown of completed, failed, pending, and processing items. This endpoint is particularly useful for real-time dashboards, monitoring tools, and operational visibility.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> getPayrollStatus(req: Request, res: Response): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> { id } = req.params;
<span class="hljs-keyword">const</span> payroll = <span class="hljs-keyword">await</span> PayrollModel.findById(<span class="hljs-built_in">parseInt</span>(id));

      <span class="hljs-keyword">if</span> (!payroll) {
        res.status(<span class="hljs-number">404</span>).json({ error: <span class="hljs-string">'Payroll not found'</span> });
        <span class="hljs-keyword">return</span>;
      }

      <span class="hljs-keyword">const</span> items = <span class="hljs-keyword">await</span> PayrollItemModel.findByPayrollId(payroll.id);

      res.json({
        data: {
          ...payroll,
          items,
          summary: {
            total: items.length,
            completed: items.filter(<span class="hljs-function">(<span class="hljs-params">i</span>) =&gt;</span> i.status === PayrollStatus.COMPLETED)
              .length,
            failed: items.filter(<span class="hljs-function">(<span class="hljs-params">i</span>) =&gt;</span> i.status === PayrollStatus.FAILED)
              .length,
            pending: items.filter(<span class="hljs-function">(<span class="hljs-params">i</span>) =&gt;</span> i.status === PayrollStatus.PENDING)
              .length,
            processing: items.filter(
              <span class="hljs-function">(<span class="hljs-params">i</span>) =&gt;</span> i.status === PayrollStatus.PROCESSING
            ).length,
          },
        },
      });
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error fetching payroll status:'</span>, error);
      res
        .status(<span class="hljs-number">500</span>)
        .json({ error: error.message || <span class="hljs-string">'Failed to fetch payroll status'</span> });
    }
}
</code></pre>
<h3 id="heading-authorizing-bulk-transfers-authorizebulktransfer">Authorizing Bulk Transfers (<code>authorizeBulkTransfer</code>)</h3>
<p>Next, we’ll implement the <code>authorizeBulkTransfer</code> endpoint. Some bulk disbursements require OTP authorization from Monnify. This endpoint accepts a batch reference and authorization code, validates their presence, and forwards them to the Monnify client for verification. Successful authorization allows the bulk transfer to proceed, while failures are clearly reported to the client.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> authorizeBulkTransfer(
req: Request,
res: Response
): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> { reference, authorizationCode, payrollId } = req.body;

      <span class="hljs-keyword">if</span> (!reference) {
        res.status(<span class="hljs-number">400</span>).json({ error: <span class="hljs-string">'Batch reference is required'</span> });
        <span class="hljs-keyword">return</span>;
      }

      <span class="hljs-keyword">if</span> (!authorizationCode) {
        res.status(<span class="hljs-number">400</span>).json({ error: <span class="hljs-string">'Authorization code (OTP) is required'</span> });
        <span class="hljs-keyword">return</span>;
      }

      <span class="hljs-keyword">const</span> result = <span class="hljs-keyword">await</span> monnifyClient.authorizeBulkTransfer(
        reference,
        authorizationCode
      );

      res.json({
        message: <span class="hljs-string">'Bulk transfer authorized successfully'</span>,
        data: result,
      });
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error authorizing bulk transfer:'</span>, error);
      res.status(<span class="hljs-number">500</span>).json({
        error: error.message || <span class="hljs-string">'Failed to authorize bulk transfer'</span>,
      });
    }
}
</code></pre>
<p>Here is what’s happening in the code:</p>
<ul>
<li><p>Firstly, we get the batch reference, OTP, and optional payroll ID from the request body.</p>
</li>
<li><p>We return a <code>400 Bad Request</code> if the reference or OTP is missing.</p>
</li>
<li><p>Next, we send the reference and OTP to Monnify to approve the bulk transfer.</p>
</li>
<li><p>If successful, return a JSON confirmation with Monnify’s response.</p>
</li>
</ul>
<h3 id="heading-checking-transaction-status-checktransactionstatus">Checking Transaction Status (<code>checkTransactionStatus</code>)</h3>
<p>This endpoint allows clients or administrators to query the status of an individual transaction using its reference. It delegates the lookup to the Monnify client and returns the raw response, making it useful for debugging, audits, or manual verification workflows.</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> checkTransactionStatus(
req: Request,
res: Response
): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">const</span> { reference } = req.params;

      <span class="hljs-keyword">if</span> (!reference) {
        res.status(<span class="hljs-number">400</span>).json({ error: <span class="hljs-string">'Transaction reference is required'</span> });
        <span class="hljs-keyword">return</span>;
      }

      <span class="hljs-keyword">const</span> status = <span class="hljs-keyword">await</span> monnifyClient.getTransactionStatus(reference);
      res.json({ data: status });
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error checking transaction status:'</span>, error);
      res
        .status(<span class="hljs-number">500</span>)
        .json({ error: error.message || <span class="hljs-string">'Failed to check transaction status'</span> });
    }
}
</code></pre>
<h3 id="heading-checking-wallet-balance-getaccountbalance">Checking Wallet Balance (<code>getAccountBalance</code>)</h3>
<p>This endpoint retrieves the current balance of the Monnify wallet associated with the payroll contract code. It’s typically used for pre-disbursement checks, monitoring available funds, or administrative reporting.</p>
<pre><code class="lang-typescript">  <span class="hljs-keyword">static</span> <span class="hljs-keyword">async</span> getAccountBalance(req: Request, res: Response): <span class="hljs-built_in">Promise</span>&lt;<span class="hljs-built_in">void</span>&gt; {
    <span class="hljs-keyword">try</span> {
      <span class="hljs-keyword">const</span> balance = <span class="hljs-keyword">await</span> monnifyClient.getAccountBalance();
      res.json({ data: balance });
    } <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
      <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error fetching account balance:'</span>, error);
      res
        .status(<span class="hljs-number">500</span>)
        .json({ error: error.message || <span class="hljs-string">'Failed to fetch account balance'</span> });
    }
  }
</code></pre>
<h3 id="heading-error-handling-and-resilience">Error Handling and Resilience</h3>
<p>All controller methods use structured <code>try–catch</code> blocks to ensure unexpected failures are logged and surfaced as controlled HTTP error responses. This approach prevents sensitive internal errors from leaking while maintaining clarity and debuggability for API consumers.</p>
<h3 id="heading-role-in-the-overall-payroll-architecture-1">Role in the Overall Payroll Architecture</h3>
<p>The <code>PayrollController</code> acts as the central coordinator of the payroll system. It bridges client requests, domain models, background job processing, and external payment services into a cohesive workflow.</p>
<p>By enforcing state transitions, delegating heavy processing to background workers, and providing reconciliation and monitoring capabilities, this controller ensures payroll execution remains reliable, auditable, and scalable in real-world production environments.</p>
<h2 id="heading-setting-up-webhook-handlers">Setting Up Webhook Handlers</h2>
<p>Webhooks are essential for receiving real-time payment status updates from Monnify. When a payment completes or fails, Monnify sends a notification to your webhook endpoint.</p>
<p>Start by creating a new file <code>src/routes/monnify.webhook.ts</code>. This file will contain everything related to handling Monnify webhook events.</p>
<pre><code class="lang-typescript">
<span class="hljs-keyword">import</span> { Router, Request, Response } <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> crypto <span class="hljs-keyword">from</span> <span class="hljs-string">'crypto'</span>;
<span class="hljs-keyword">import</span> {
PayrollItemModel,
PayrollModel,
PayrollStatus,
} <span class="hljs-keyword">from</span> <span class="hljs-string">'../models/payroll'</span>;

<span class="hljs-keyword">const</span> router = Router();

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">verifySignature</span>(<span class="hljs-params">req: Request</span>): <span class="hljs-title">boolean</span> </span>{
<span class="hljs-keyword">const</span> signature = req.headers[<span class="hljs-string">'monnify-signature'</span>] <span class="hljs-keyword">as</span> <span class="hljs-built_in">string</span>;
<span class="hljs-keyword">if</span> (!signature) <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;

<span class="hljs-keyword">const</span> secret = process.env.MONNIFY_WEBHOOK_SECRET!;
<span class="hljs-keyword">const</span> hash = crypto
.createHmac(<span class="hljs-string">'sha512'</span>, secret)
.update(<span class="hljs-built_in">JSON</span>.stringify(req.body))
.digest(<span class="hljs-string">'hex'</span>);

<span class="hljs-keyword">return</span> hash === signature;
}

router.post(<span class="hljs-string">'/monnify/webhook'</span>, <span class="hljs-keyword">async</span> (req: Request, res: Response) =&gt; {
<span class="hljs-keyword">try</span> {
<span class="hljs-built_in">console</span>.log(<span class="hljs-string">'Monnify Webhook:'</span>, <span class="hljs-built_in">JSON</span>.stringify(req.body, <span class="hljs-literal">null</span>, <span class="hljs-number">2</span>));

    <span class="hljs-keyword">const</span> { eventType, eventData } = req.body;

    <span class="hljs-keyword">if</span> (!eventData?.reference) {
      <span class="hljs-built_in">console</span>.warn(<span class="hljs-string">'Missing reference, ignoring webhook'</span>);
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">200</span>).send(<span class="hljs-string">'Ignored'</span>);
    }

    <span class="hljs-keyword">const</span> paymentReference = eventData.reference;
    <span class="hljs-keyword">const</span> transactionReference = eventData.transactionReference;
    <span class="hljs-keyword">const</span> description = eventData.transactionDescription || <span class="hljs-string">''</span>;

    <span class="hljs-comment">// Parse our reference format: PAYROLL_{payrollId}_{itemId}</span>
    <span class="hljs-keyword">const</span> [prefix, payrollIdStr, itemIdStr] = paymentReference.split(<span class="hljs-string">'_'</span>);

    <span class="hljs-keyword">if</span> (prefix !== <span class="hljs-string">'PAYROLL'</span>) {
      <span class="hljs-built_in">console</span>.warn(<span class="hljs-string">'Invalid payment reference format:'</span>, paymentReference);
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">200</span>).send(<span class="hljs-string">'Ignored'</span>);
    }

    <span class="hljs-keyword">const</span> payrollId = <span class="hljs-built_in">Number</span>(payrollIdStr);
    <span class="hljs-keyword">const</span> itemId = <span class="hljs-built_in">Number</span>(itemIdStr);

    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">isNaN</span>(payrollId) || <span class="hljs-built_in">isNaN</span>(itemId)) {
      <span class="hljs-built_in">console</span>.warn(<span class="hljs-string">'Invalid payroll/item IDs:'</span>, paymentReference);
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">200</span>).send(<span class="hljs-string">'Ignored'</span>);
    }

    <span class="hljs-keyword">const</span> item = <span class="hljs-keyword">await</span> PayrollItemModel.findById(itemId);

    <span class="hljs-keyword">if</span> (!item) {
      <span class="hljs-built_in">console</span>.warn(<span class="hljs-string">'Payroll item not found:'</span>, itemId);
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">200</span>).send(<span class="hljs-string">'Ignored'</span>);
    }

    <span class="hljs-comment">// Idempotency check - don't process already finalized items</span>
    <span class="hljs-keyword">if</span> (
      item.status === PayrollStatus.COMPLETED ||
      item.status === PayrollStatus.FAILED
    ) {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Item <span class="hljs-subst">${itemId}</span> already finalized (<span class="hljs-subst">${item.status}</span>)`</span>);
      <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">200</span>).send(<span class="hljs-string">'Already processed'</span>);
    }

    <span class="hljs-comment">// Update status based on event type</span>
    <span class="hljs-keyword">if</span> (
      eventType === <span class="hljs-string">'SUCCESSFUL_DISBURSEMENT'</span> ||
      eventData.status === <span class="hljs-string">'SUCCESS'</span>
    ) {
      <span class="hljs-keyword">await</span> PayrollItemModel.updateStatus(
        itemId,
        PayrollStatus.COMPLETED,
        transactionReference
      );
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`✅ Payroll item <span class="hljs-subst">${itemId}</span> COMPLETED`</span>);
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (
      eventType === <span class="hljs-string">'FAILED_DISBURSEMENT'</span> ||
      eventType === <span class="hljs-string">'REVERSED_DISBURSEMENT'</span> ||
      eventData.status === <span class="hljs-string">'FAILED'</span>
    ) {
      <span class="hljs-keyword">await</span> PayrollItemModel.updateStatus(
        itemId,
        PayrollStatus.FAILED,
        transactionReference,
        description
      );
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Payroll item <span class="hljs-subst">${itemId}</span> FAILED`</span>);
    } <span class="hljs-keyword">else</span> {
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Unhandled Monnify eventType: <span class="hljs-subst">${eventType}</span>`</span>);
    }

    <span class="hljs-comment">// Update overall payroll stats</span>
    <span class="hljs-keyword">await</span> updatePayrollStats(payrollId);

    <span class="hljs-keyword">return</span> res.status(<span class="hljs-number">200</span>).send(<span class="hljs-string">'OK'</span>);

} <span class="hljs-keyword">catch</span> (error: <span class="hljs-built_in">any</span>) {
<span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Monnify webhook error:'</span>, error.message);
<span class="hljs-keyword">return</span> res.status(<span class="hljs-number">200</span>).send(<span class="hljs-string">'OK'</span>); <span class="hljs-comment">// Always return 200 to prevent retries</span>
}
});

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> router;

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">updatePayrollStats</span>(<span class="hljs-params">payrollId: <span class="hljs-built_in">number</span></span>) </span>{
<span class="hljs-keyword">const</span> items = <span class="hljs-keyword">await</span> PayrollItemModel.findByPayrollId(payrollId);

<span class="hljs-keyword">const</span> completed = items.filter(
<span class="hljs-function">(<span class="hljs-params">i</span>) =&gt;</span> i.status === PayrollStatus.COMPLETED
).length;

<span class="hljs-keyword">const</span> failed = items.filter(<span class="hljs-function">(<span class="hljs-params">i</span>) =&gt;</span> i.status === PayrollStatus.FAILED).length;

<span class="hljs-keyword">let</span> status = PayrollStatus.PROCESSING;

<span class="hljs-keyword">if</span> (completed === items.length) {
status = PayrollStatus.COMPLETED;
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (failed === items.length) {
status = PayrollStatus.FAILED;
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (completed &gt; <span class="hljs-number">0</span>) {
status = PayrollStatus.PARTIALLY_COMPLETED;
}

<span class="hljs-keyword">await</span> PayrollModel.updateStatus(payrollId, status, completed, failed);
}
</code></pre>
<p>Key webhook implementation details:</p>
<ol>
<li><p><strong>Signature verification</strong>: The <code>verifySignature</code> function validates that webhooks actually come from Monnify.</p>
</li>
<li><p><strong>Idempotency</strong>: The handler checks if an item is already finalized before processing.</p>
</li>
<li><p><strong>Always return 200</strong>: Even on errors, return 200 to prevent Monnify from retrying indefinitely.</p>
</li>
<li><p><strong>Reference parsing</strong>: Our reference format <code>PAYROLL_{payrollId}_{itemId}</code> lets us identify which payment item to update.</p>
</li>
</ol>
<h2 id="heading-wiring-up-routes">Wiring Up Routes</h2>
<h3 id="heading-employee-routes">Employee Routes</h3>
<p>We’ll start by defining routes for employee management. These routes expose CRUD operations for employees and simply delegate the actual logic to the <code>EmployeeController</code>.</p>
<p>Create the file <code>src/routes/employee.routes.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Router } <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> { EmployeeController } <span class="hljs-keyword">from</span> <span class="hljs-string">'../controllers/employee.controller'</span>;

<span class="hljs-keyword">const</span> router = Router();

router.post(<span class="hljs-string">'/'</span>, EmployeeController.createEmployee);
router.get(<span class="hljs-string">'/'</span>, EmployeeController.getAllEmployees);
router.get(<span class="hljs-string">'/:id'</span>, EmployeeController.getEmployeeById);
router.put(<span class="hljs-string">'/:id'</span>, EmployeeController.updateEmployee);
router.delete(<span class="hljs-string">'/:id'</span>, EmployeeController.deleteEmployee);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> router;
</code></pre>
<p>What this gives us:</p>
<ul>
<li><p>A clean <code>/api/employees</code> entry point for all employee-related operations</p>
</li>
<li><p>Clear separation between routing (URLs) and business logic (controllers)</p>
</li>
<li><p>A predictable REST structure that’s easy to extend later</p>
</li>
</ul>
<h3 id="heading-payroll-routes">Payroll Routes</h3>
<p>Next, we define routes for payroll operations. Payroll is more complex than employees, so this router exposes endpoints for creation, processing, reconciliation, authorization, and monitoring.</p>
<p>Create the file <code>src/routes/payroll.routes.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> { Router } <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> { PayrollController } <span class="hljs-keyword">from</span> <span class="hljs-string">'../controllers/payroll.controller'</span>;

<span class="hljs-keyword">const</span> router = Router();

router.post(<span class="hljs-string">'/'</span>, PayrollController.createPayroll);
router.get(<span class="hljs-string">'/'</span>, PayrollController.getAllPayrolls);
router.get(<span class="hljs-string">'/:id'</span>, PayrollController.getPayrollById);
router.post(<span class="hljs-string">'/:id/process'</span>, PayrollController.processPayroll);
router.post(<span class="hljs-string">'/batch/authorize'</span>, PayrollController.authorizeBulkTransfer);
router.get(<span class="hljs-string">'/:id/status'</span>, PayrollController.getPayrollStatus);
router.get(
  <span class="hljs-string">'/transaction/:reference/status'</span>,
  PayrollController.checkTransactionStatus
);
router.get(<span class="hljs-string">'/account/balance'</span>, PayrollController.getAccountBalance);
router.post(<span class="hljs-string">'/:id/reconcile'</span>, PayrollController.reconcilePayroll);

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> router;
</code></pre>
<p>What’s happening here:</p>
<ul>
<li><p>Each route maps directly to a well-defined payroll operation</p>
</li>
<li><p>Long-running or sensitive actions (processing, reconciliation, authorization) are clearly separated</p>
</li>
<li><p>Monitoring and operational endpoints (status, transaction lookup, balance checks) are first-class citizens</p>
</li>
</ul>
<h3 id="heading-main-application-entry-point">Main Application Entry Point</h3>
<p>With all routes defined, we now bring everything together in the main application file. This is where we configure middleware, register routes, and start the server.</p>
<p>Create the file <code>src/index.ts</code>:</p>
<pre><code class="lang-typescript"><span class="hljs-keyword">import</span> express, { Application, Request, Response } <span class="hljs-keyword">from</span> <span class="hljs-string">'express'</span>;
<span class="hljs-keyword">import</span> cors <span class="hljs-keyword">from</span> <span class="hljs-string">'cors'</span>;
<span class="hljs-keyword">import</span> helmet <span class="hljs-keyword">from</span> <span class="hljs-string">'helmet'</span>;
<span class="hljs-keyword">import</span> dotenv <span class="hljs-keyword">from</span> <span class="hljs-string">'dotenv'</span>;
<span class="hljs-keyword">import</span> path <span class="hljs-keyword">from</span> <span class="hljs-string">'path'</span>;
<span class="hljs-keyword">import</span> { pool } <span class="hljs-keyword">from</span> <span class="hljs-string">'./config/database'</span>;
<span class="hljs-keyword">import</span> employeeRoutes <span class="hljs-keyword">from</span> <span class="hljs-string">'./routes/employee.routes'</span>;
<span class="hljs-keyword">import</span> payrollRoutes <span class="hljs-keyword">from</span> <span class="hljs-string">'./routes/payroll.routes'</span>;
<span class="hljs-keyword">import</span> monnifyWebhookRoutes <span class="hljs-keyword">from</span> <span class="hljs-string">'./routes/monnify.webhook'</span>;

dotenv.config();

<span class="hljs-keyword">const</span> app: Application = express();
<span class="hljs-keyword">const</span> PORT = process.env.PORT || <span class="hljs-number">3008</span>;

<span class="hljs-comment">// Middleware</span>
app.use(
  helmet({
    contentSecurityPolicy: <span class="hljs-literal">false</span>,
  })
);
app.use(
  cors({
    origin: <span class="hljs-string">'*'</span>,
    methods: [<span class="hljs-string">'GET'</span>, <span class="hljs-string">'POST'</span>, <span class="hljs-string">'PUT'</span>, <span class="hljs-string">'DELETE'</span>, <span class="hljs-string">'OPTIONS'</span>],
    allowedHeaders: [<span class="hljs-string">'Content-Type'</span>, <span class="hljs-string">'Authorization'</span>],
  })
);
app.use(express.json());
app.use(express.urlencoded({ extended: <span class="hljs-literal">true</span> }));

<span class="hljs-comment">// Health check endpoint</span>
app.get(<span class="hljs-string">'/health'</span>, <span class="hljs-keyword">async</span> (req: Request, res: Response) =&gt; {
  <span class="hljs-keyword">try</span> {
    <span class="hljs-keyword">await</span> pool.query(<span class="hljs-string">'SELECT 1'</span>);
    res.json({ status: <span class="hljs-string">'healthy'</span>, database: <span class="hljs-string">'connected'</span> });
  } <span class="hljs-keyword">catch</span> (error) {
    res.status(<span class="hljs-number">500</span>).json({ status: <span class="hljs-string">'unhealthy'</span>, database: <span class="hljs-string">'disconnected'</span> });
  }
});

<span class="hljs-comment">// Routes</span>
app.use(<span class="hljs-string">'/api/employees'</span>, employeeRoutes);
app.use(<span class="hljs-string">'/api/payrolls'</span>, payrollRoutes);
app.use(<span class="hljs-string">'/api'</span>, monnifyWebhookRoutes);

<span class="hljs-comment">// 404 handler</span>
app.use(<span class="hljs-function">(<span class="hljs-params">req: Request, res: Response</span>) =&gt;</span> {
  res.status(<span class="hljs-number">404</span>).json({ error: <span class="hljs-string">'Route not found'</span> });
});

<span class="hljs-comment">// Error handler</span>
app.use(<span class="hljs-function">(<span class="hljs-params">err: <span class="hljs-built_in">any</span>, req: Request, res: Response, next: <span class="hljs-built_in">any</span></span>) =&gt;</span> {
  <span class="hljs-built_in">console</span>.error(<span class="hljs-string">'Error:'</span>, err);
  res.status(err.status || <span class="hljs-number">500</span>).json({
    error: err.message || <span class="hljs-string">'Internal server error'</span>,
  });
});

app.listen(PORT, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Server is running on port <span class="hljs-subst">${PORT}</span>`</span>);
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">`Environment: <span class="hljs-subst">${process.env.NODE_ENV || <span class="hljs-string">'development'</span>}</span>`</span>);
});

<span class="hljs-comment">// Graceful shutdown</span>
process.on(<span class="hljs-string">'SIGTERM'</span>, <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'SIGTERM signal received: closing HTTP server'</span>);
  <span class="hljs-keyword">await</span> pool.end();
  process.exit(<span class="hljs-number">0</span>);
});

process.on(<span class="hljs-string">'SIGINT'</span>, <span class="hljs-keyword">async</span> () =&gt; {
  <span class="hljs-built_in">console</span>.log(<span class="hljs-string">'SIGINT signal received: closing HTTP server'</span>);
  <span class="hljs-keyword">await</span> pool.end();
  process.exit(<span class="hljs-number">0</span>);
});
</code></pre>
<h2 id="heading-testing-the-system">Testing the System</h2>
<p>Now let's test the complete payroll flow.</p>
<p>Start the application:</p>
<pre><code class="lang-bash">docker-compose up -d
npm run dev
</code></pre>
<p>Create employees:</p>
<pre><code class="lang-bash">curl -X POST http://localhost:3008/api/employees \
  -H <span class="hljs-string">"Content-Type: application/json"</span> \
  -d <span class="hljs-string">'{
    "name": "John Doe",
    "email": "john.doe@company.com",
    "salary": 50000,
    "account_number": "0123456789",
    "bank_code": "058",
    "bank_name": "GTBank"
  }'</span>
</code></pre>
<p>Create a few more employees with different salaries to see how it’s handled.</p>
<p>Create a payroll:</p>
<pre><code class="lang-bash">curl -X POST http://localhost:3008/api/payrolls \
  -H <span class="hljs-string">"Content-Type: application/json"</span> \
  -d <span class="hljs-string">'{
    "payroll_period": "2024-12"
  }'</span>
</code></pre>
<p>This creates a payroll with all active employees.</p>
<p>Process the payroll:</p>
<pre><code class="lang-bash">curl -X POST http://localhost:3008/api/payrolls/1/process
</code></pre>
<p>This queues the payroll for background processing. The system will:</p>
<ol>
<li><p>Create a bulk transfer request to Monnify</p>
</li>
<li><p>Update each payroll item with a transaction reference</p>
</li>
<li><p>Wait for webhooks to update final status</p>
</li>
</ol>
<p>Authorize the bulk transfer (if OTP is required):</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766392280287/4d8ae61f-4ccf-4d63-86a1-a6f72d7286e1.png" alt="Monnify payroll authorization OTP email" class="image--center mx-auto" width="2358" height="1460" loading="lazy"></p>
<p>After processing, Monnify sends an OTP to your registered email. Use it to authorize:</p>
<pre><code class="lang-bash">curl -X POST http://localhost:3008/api/payrolls/batch/authorize \
  -H <span class="hljs-string">"Content-Type: application/json"</span> \
  -d <span class="hljs-string">'{
    "reference": "BATCH_1702123456789",
    "authorizationCode": "123456",
    "payrollId": 1
  }'</span>
</code></pre>
<p>Check the payroll status:</p>
<pre><code class="lang-bash">curl http://localhost:3008/api/payrolls/1/status
</code></pre>
<p>This returns detailed status including a summary of completed, failed, and pending items.</p>
<p>Now, reconcile if needed – if webhooks were missed or you need to sync status:</p>
<pre><code class="lang-bash">curl -X POST http://localhost:3008/api/payrolls/1/reconcile
</code></pre>
<h2 id="heading-setting-up-webhooks-for-production">Setting Up Webhooks for Production</h2>
<p>For Monnify to send webhooks to your local development environment, you'll need to expose your local server. You can use ngrok:</p>
<pre><code class="lang-bash">ngrok http 3008
</code></pre>
<p>Then configure the webhook URL in your <a target="_blank" href="https://app.monnify.com/developer#webhook-urls">Monnify dashboard</a>:</p>
<pre><code class="lang-plaintext">https://your-ngrok-url.ngrok.io/api/monnify/webhook
</code></pre>
<p>For production, use your actual server URL and ensure HTTPS is enabled.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766392444369/440bc1a9-7c70-42b0-9157-892f1ef07861.png" alt="Monnify webhook URL configuration" class="image--center mx-auto" width="3024" height="1722" loading="lazy"></p>
<p>Then when transactions are successful it will be revealed on the monnify dashboard as well as the transactions that failed.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1766392958199/e8abaa75-5a2f-44fd-b322-b110cf71e92d.png" alt="Monnify dashboard with payroll transaction status" class="image--center mx-auto" width="3024" height="2476" loading="lazy"></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>You've built a complete payroll system that:</p>
<ul>
<li><p>Manages employees with their bank account details</p>
</li>
<li><p>Creates payroll batches with automatic amount calculation</p>
</li>
<li><p>Processes bulk payments using Monnify's disbursement API</p>
</li>
<li><p>Uses background jobs to prevent request timeouts</p>
</li>
<li><p>Handles webhooks for real-time status updates</p>
</li>
<li><p>Supports reconciliation to ensure data consistency</p>
</li>
</ul>
<h3 id="heading-key-takeaways">Key Takeaways</h3>
<ol>
<li><p><strong>Background jobs are essential</strong>: Processing payments synchronously would timeout for large payrolls. Bull and Redis provide reliable async processing.</p>
</li>
<li><p><strong>Idempotency matters</strong>: Both the webhook handler and reconciliation process check current status before updating, preventing duplicate processing.</p>
</li>
<li><p><strong>Bulk transfers save time</strong>: Monnify's batch API lets you process hundreds of payments with a single OTP authorization.</p>
</li>
<li><p><strong>Status tracking is critical</strong>: The system tracks status at both the payroll and individual item level, making it easy to identify and handle failures.</p>
</li>
<li><p><strong>Reconciliation is your safety net</strong>: When webhooks fail or get delayed, the reconciliation endpoint ensures your database stays in sync with actual payment status.</p>
</li>
</ol>
<h3 id="heading-references">References:</h3>
<ul>
<li><a target="_blank" href="https://developers.monnify.com/">Monnify Docs</a></li>
</ul>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
