Tag: React

  • Caching in the Next.js App Router: Which Layer Is Answering?

    Caching in the Next.js App Router: Which Layer Is Answering?

    “I changed the API URL and the site still shows the old posts.” If you have built a Next.js site on top of a headless CMS, you have probably said something like this. The App Router caches aggressively and in several layers, and most confusing bugs come from not knowing which layer is answering. This is the mental model I use.

    Layer 1: environment variables are read at start or build time

    Values in .env.local are loaded when the dev server starts or when you build. Editing the file while the server runs changes nothing until you restart. Also check which folder the server runs from: each project folder (and each git worktree) reads its own env file.

    Anything prefixed NEXT_PUBLIC_ is inlined into the browser bundle at build time, so it is both public and frozen. Secrets such as API tokens must never use that prefix.

    Layer 2: pages rendered at build time

    Routes that can be pre-rendered are generated during next build. With generateStaticParams, every slug you return becomes static HTML. That makes pages instant, but it also means:

    • The CMS must be reachable from the build machine.
    • Content changes only appear after revalidation or a rebuild.
    • Config read at build time, such as allowed image hosts in next.config.js, needs a rebuild to change.

    Layer 3: the data cache

    For data that does not come from a plain fetch, such as an axios client or an SDK, unstable_cache wraps a function and stores its result on the server:

    import { unstable_cache } from "next/cache";
    
    export const getPosts = unstable_cache(
      async () => fetchPostsFromCms(),
      ["cms-posts", API_BASE, String(CATEGORY_ID)],
      { revalidate: 120 }
    );

    Two parts deserve attention:

    • Key parts. The array is part of the cache key. Include everything that changes the result, such as the API base URL and category ID. If you leave the URL out, switching environments can keep serving the old environment’s data.
    • Revalidate. After the window expires, the next request still gets the cached value while a fresh one is fetched in the background. If that fetch fails, the stale value keeps being served, which is usually what you want for a blog, but can look like “nothing changed”.

    Newer Next.js versions also offer the "use cache" directive with cache life profiles. The concepts are the same: a key, a lifetime and a way to invalidate.

    Layer 4: on-demand invalidation

    Time-based revalidation is a safety net, not a publishing workflow. For instant updates, tag cached data and invalidate the tag when the CMS changes:

    // app/api/revalidate/route.ts
    import { revalidateTag } from "next/cache";
    
    export async function POST(request: Request) {
      const secret = request.headers.get("x-revalidate-secret");
      if (secret !== process.env.REVALIDATE_SECRET) {
        return new Response("Unauthorized", { status: 401 });
      }
      revalidateTag("cms-posts");
      return Response.json({ revalidated: true });
    }

    Point a CMS webhook (for WordPress, a small plugin or a save_post hook) at this route, and published posts show up within seconds without waiting for the timer.

    Layer 5: the browser and the router

    The client-side router keeps recently visited segments in memory, so pressing back can show what you saw a minute ago. A hard refresh rules this layer out quickly when debugging.

    A debugging checklist

    1. Restart the dev server and confirm it runs from the folder whose env file you edited.
    2. Check that the variable is read server-side and not inlined with NEXT_PUBLIC_.
    3. Confirm the cache key includes the values you changed.
    4. Look at the server logs for failed revalidation fetches.
    5. For production, ask whether the page was built statically and needs revalidation or a rebuild.

    Caching is what makes a headless setup feel fast. Knowing which layer is answering is what makes it feel predictable.

  • Frontend Architecture That Survives the Second Year

    Frontend Architecture That Survives the Second Year

    Most frontend codebases are pleasant for the first few months. Then the team grows, features pile up, and suddenly a small change touches nine files and breaks a page nobody was looking at. The decisions that prevent that are rarely about frameworks. They are about boundaries, data flow and habits.

    Organise by feature, not by file type

    A components/ folder with two hundred files, a hooks/ folder and a utils/ folder tells you what things are, not what they are for. Grouping by feature keeps related code together and makes ownership obvious:

    features/
      checkout/
        checkout-page.tsx
        payment-form.tsx
        use-cart-total.ts
        checkout.api.ts
        checkout.test.tsx
      profile/
        ...
    shared/
      ui/        # buttons, inputs, dialogs
      lib/       # formatting, dates, fetch client

    A simple rule keeps this healthy: features may import from shared, but not from each other. When two features need the same thing, promote it to shared deliberately.

    Keep state close to where it is used

    Global state is convenient on day one and expensive on day three hundred. Ask where each piece of state really belongs:

    • Server data belongs in a data layer with caching, such as React Query, SWR or server components, not copied into a global store.
    • URL state such as filters, tabs and pagination belongs in the URL, so it survives refreshes and can be shared.
    • Local UI state such as an open dropdown belongs in the component.
    • Truly global client state, like the current theme or a 3D scene’s settings, is the small remainder that justifies a store.

    When high-frequency values, such as pointer position in a render loop, change every frame, keep them out of React state entirely and read them where they are needed.

    Make components boring to use

    A good shared component has a small, predictable API. Prefer composition to configuration: a Card with slots ages better than a Card with twenty boolean props.

    // Hard to extend
    <Card title="Plan" showBadge badgeText="New" footerAlign="right" />
    
    // Easy to extend
    <Card>
      <Card.Header>Plan <Badge>New</Badge></Card.Header>
      <Card.Footer className="justify-end">...</Card.Footer>
    </Card>

    Let types document the edges

    TypeScript pays off most at boundaries: API responses, component props and shared utilities. Validate data where it enters the app, then trust the types inside. A runtime schema at the API boundary catches backend changes before they become blank screens:

    import { z } from "zod";
    
    const Post = z.object({
      id: z.number(),
      title: z.string(),
      publishedAt: z.string().datetime(),
    });
    
    export type Post = z.infer<typeof Post>;
    
    export async function getPost(id: number): Promise<Post> {
      const response = await fetch(`/api/posts/${id}`);
      return Post.parse(await response.json());
    }

    Test behaviour, not implementation

    Tests that assert on internal state break every refactor and catch few real bugs. Tests that act like a user (type into the field, click the button, expect the message) survive refactors and protect what matters. A practical mix:

    • Unit tests for pure logic: pricing, formatting, reducers.
    • Component tests with Testing Library for important interactions.
    • A handful of end-to-end tests for the flows that make money or keep users safe.

    Automate the arguments away

    Formatting, import order and common mistakes should never come up in code review. A formatter, a strict linter and type checks in CI free reviewers to discuss design and behaviour. Add a performance budget too: a bundle-size check on pull requests catches the accidental 300 KB dependency before it ships.

    Write down the decisions

    The hardest knowledge to recover is why something was built a certain way. A short decision record (context, options, choice and consequences) takes ten minutes and saves hours when someone later wonders whether that odd pattern is safe to remove.

    In short

    • Group by feature; keep cross-feature imports out.
    • Put state where it belongs: server, URL, component, then store.
    • Design components for composition.
    • Validate at the edges, trust types inside.
    • Test what users do.
    • Automate style; document decisions.

    Architecture is not about predicting the future. It is about making the next change easy, and the one after that.

  • Lovers Finder

    Lovers Finder

    Lovers Finder is a modern dating platform built as a Telegram Mini App, designed to make meeting new people easier, more accessible and more enjoyable. It is an independently developed product: I designed and built it myself as the first step toward running my own business.

    The idea

    The goal is to help people discover meaningful connections through a seamless, intuitive experience, without installing a separate application. Everything runs inside Telegram, where the users already are.

    Key features

    • Smart profile discovery and matching.
    • Personalised user profiles with photo uploads.
    • Real-time messaging between matched users.
    • User verification and profile trust features.
    • VIP membership and premium features.
    • Seamless integration with the Telegram ecosystem.
    • A modern, responsive UI focused on user experience.

    How it is built

    The product is built with a focus on scalability, performance and user experience:

    • Next.js and React for the Mini App interface
    • Node.js and Express for the API
    • MongoDB for data
    • Cloudflare R2 for photo storage
    • Telegram Mini Apps API for the integration with Telegram

    Finding your partner is on us, the first date is on you.

    Explore Lovers Finder on Telegram.

  • Bitfa Website With React

    Bitfa Website With React

    Bitfa.io is a platform that provides solutions in the cryptocurrency and finance sectors. Its mission is to give users seamless access to digital assets, financial services and secure trading tools.

    My role

    I joined Bitfa as a Senior Front-End Developer, working with the team to build a dynamic, user-friendly platform that combines innovative technology with an intuitive interface. I built the website with React.

    Tech stack

    • React

    Visit bitfa.io.

  • Nexu.co

    Nexu.co

    Nexu is a startup platform where users book online consultations with specialists in psychology, medicine and nutrition. I worked on it as a front-end developer for over two years, building the web app that patients use to find a specialist, book a session and manage their care.

    The product

    The web app brings three kinds of consultation into one place. Users browse specialists, compare their profiles, pick a time and attend their sessions online. Everything has to feel calm and trustworthy, because people often arrive at a stressful moment.

    My role

    • Built the user-facing web app with React and Next.js, using TypeScript throughout.
    • Connected the interface to the platform’s REST APIs for specialists, bookings and user accounts.
    • Developed responsive interfaces that work as well on phones as on desktop.
    • Implemented the app as a fast single-page experience to improve performance and usability.

    Tech stack

    • React and Next.js
    • TypeScript
    • Web APIs and REST APIs

    Visit the product at nexu.co.

  • Nexu Dashboard Panel

    Nexu Dashboard Panel

    The Nexu Dashboard Panel is the HR dashboard for Nexu’s partner companies. It lets each company manage its users, their access to consultations and the other settings that come with offering Nexu as an employee benefit. I was the front-end developer on the panel.

    The problem

    Partner companies needed one place to onboard employees, manage their accounts and keep track of how the service was being used, without contacting Nexu support for every change.

    My role

    • Built the full HR dashboard with React, Next.js and TypeScript.
    • Created interactive dashboards with real-time data visualisation.
    • Used MUI for a consistent, accessible component base, and Redux for shared application state.
    • Contributed to a custom UI library that increased developer productivity and code reuse across Nexu projects.
    • Integrated the panel with the platform’s REST APIs.

    Tech stack

    • React, Next.js and TypeScript
    • MUI
    • Redux
    • Web APIs and REST APIs