Web razvoj
ReactNext.jsFrontend arhitekturaSkalabilnostOrganizacija kodaTestiranje

Moderna React frontend arhitektura: moduli po značajkama, granice i skalabilnost

AO
Adrijan Omićević
·12 min čitanja

# Što ćeš izgraditi u ovom vodiču#

Implementirat ćeš feature-based strategiju strukture mapa za React frontend arhitekturu koja skalira i nakon prvih 3 do 5 značajki, bez pretvaranja u labirint zajedničkih utila i kružnih importa.

Naučit ćeš gdje smjestiti API klijente, UI primitive, domensku logiku, testove i poprečne brige (cross-cutting concerns) u Reactu i Next.js-u. Dobit ćeš i pravila ovisnosti koja možeš provoditi s ESLintom, uz primjere koje možeš izravno kopirati u svoj projekt.

Ova struktura je namijenjena timovima gdje:

  • Aplikacija će rasti izvan jednog developera.
  • Više značajki se isporučuje paralelno.
  • Želiš da refaktori budu lokalizirani umjesto “diraj 40 datoteka u 10 mapa”.

# Zašto feature-based arhitektura pobjeđuje na skali#

Struktura mapa postaje arhitektura onog trenutka kad imaš:

  • više od 10 ekrana,
  • više od 2 developera,
  • više od 1 integracije,
  • više od 1 izdanja tjedno.

Feature-based pristup smanjuje trošak koordinacije jer promjene koda najčešće ostaju unutar modula koji odgovara načinu na koji se o proizvodu govori: onboarding, naplata, pretraga, checkout, postavke računa.

Trend u industriji to potvrđuje. 2024 State of JS survey navodi da je React i dalje najkorištenija UI biblioteka, a najčešća bol koju React timovi prijavljuju je održavanje i rastuća kompleksnost kako aplikacije rastu. Feature-based granice ne uklanjaju kompleksnost, ali je zadržavaju pod kontrolom.

Ako ti je važna i konzistentnost UI primitiva te skalabilan dizajn komponenti, upari ovaj vodič s člankom React arhitektura komponenti za skalabilne design sisteme.

# Osnovna načela: granice, ovisnosti i javni API-ji#

Feature-based arhitektura radi samo ako su granice stvarne. To znači da definiraš:

  1. 1
    Što svaki modul posjeduje
  2. 2
    Što smije importati
  3. 3
    Što javno izlaže

Načelo 1: Moduli posjeduju poslovnu sposobnost, ne samo UI#

Feature modul bi trebao uključivati:

  • UI na razini rute (pages, screens)
  • stanje značajke i orkestraciju
  • domenska pravila za tu značajku
  • API pozive značajke
  • komponente specifične za značajku
  • testove značajke

Ako domensku logiku držiš u nasumičnim zajedničkim utilima, ponovno stvaraš monolit, samo s više mapa.

Načelo 2: Ovisnosti teku u jednom smjeru#

Tvoja struktura treba spriječiti “svaštaru” shared sloj o kojem svi ovise i koji svi mijenjaju.

Jednostavno pravilo koje dobro skalira:

  • shared ne ovisi ni o čemu
  • entities ovisi o shared
  • features ovisi o entities i shared
  • app slaže features i globalne providere

Ovo je blisko poznatim obrascima poput feature-sliced designa, ali pojednostavljeno kako bi ostalo pragmatično.

Načelo 3: Svaki modul exporta javni API#

Ako drugi moduli importaju duboke putanje, granice su imaginarne. Želiš importe poput:

TypeScript
import { CheckoutPage } from "@/features/checkout";
import { formatMoney } from "@/shared/lib/money";

a ne:

TypeScript
import { formatMoney } from "@/shared/lib/money/formatMoney";
import { CheckoutForm } from "@/features/checkout/ui/forms/CheckoutForm";

Duboki importi zaobilaze odluke i stvaraju “spaghetti coupling”.

🎯 Ključna poruka: Struktura mapa je arhitektura tek kad provodiš smjer ovisnosti i javne exporte.

# Preporučena struktura mapa za React i Next.js#

Ova struktura podržava:

  • React SPA s React Routerom
  • Next.js App Router s React Server Components

Koristi src kao korijen i drži Next.js app na rootu projekta.

Layout na visokoj razini#

SlojOdgovornostSmije importatiNe smije importati
appKompozicija ruta, provideri, bootstrappingfeatures, entities, sharedinterne dijelove feature-a
featuresKorisnički vidljive sposobnosti, orkestracijaentities, sharedinterne dijelove drugih feature-a
entitiesOsnovni domenski modeli i ponovno upotrebljiv entity UIsharedfeatures, app
sharedUI primitive, libovi, config, bazni API klijentništa (ili samo npm ovisnosti)app, features, entities
testsCross-feature test utili, fixturessharedinterne dijelove feature-a (idealno)

Primjer stabla mapa#

Bash
src/
  app/
    providers/
    routes/
  features/
    auth/
    checkout/
    search/
  entities/
    user/
    product/
    order/
  shared/
    api/
    ui/
    lib/
    config/
    types/
  tests/
    fixtures/
    helpers/

Za Next.js App Router tipično ćeš imati:

Bash
app/
  (marketing)/
  (app)/
  api/
src/
  features/
  entities/
  shared/

Next.js app direktorij je routing kojim upravlja framework. Stvarni product kod živi u feature modulima pod src.

ℹ️ Napomena: U Next.js-u izbjegavaj stavljati poslovnu logiku izravno u app rute. Route datoteke tretiraj kao kompozicijsko “ljepilo”, a logiku drži u src/features kako bi ostala prenosiva i testabilna.

# Kako dizajnirati feature module#

Feature modul bi trebao djelovati kao mali paket sa svojom javnom površinom.

Predložak feature modula#

Bash
src/features/checkout/
  index.ts
  model/
    useCheckout.ts
    checkoutMachine.ts
    selectors.ts
  api/
    checkoutApi.ts
    types.ts
  ui/
    CheckoutPage.tsx
    CheckoutForm.tsx
    components/
      PaymentMethodPicker.tsx
  lib/
    pricing.ts
    validation.ts
  tests/
    checkout.test.ts
    pricing.test.ts

Što ide gdje#

MapaStavi ovdjePrimjer
apiAPI pozivi specifični za feature i mapiranje DTO-acreateOrder, applyCoupon
modelstanje, hookovi, orkestratoriuseCheckout, XState machineovi
uiekrani i komponente feature-aCheckoutPage, CheckoutForm
libčiste funkcije koje se koriste unutar feature-avalidateAddress, calcTotals
teststestovi feature-a blizu kodapricing.test.ts
index.tssamo javni exportiexport { CheckoutPage }

Primjer javnog API-ja feature-a#

TypeScript
// src/features/checkout/index.ts
export { CheckoutPage } from "./ui/CheckoutPage";
export { useCheckout } from "./model/useCheckout";
export type { CheckoutDraft } from "./model/types";

Exportaj samo ono o čemu drugi moduli trebaju ovisiti. Sve ostalo ostaje interno.

⚠️ Upozorenje: Ako feature-i importaju druge feature-e direktno, brzo ćeš dobiti cikluse poput checkout -> auth -> cart -> checkout. Radije komuniciraj među feature-ima kroz shared apstrakcije ili podigni orkestraciju u app.

# Gdje pripadaju API klijenti (bez stvaranja globalnog “God” modula)#

Većina timova krene sa src/api i završi s 200 datoteka i nejasnim vlasništvom. Umjesto toga, razdvoji “transport” od “endpointa”.

Zajednički API transport#

Drži minimalan HTTP klijent u shared/api. Trebao bi biti dosadan i stabilan.

Bash
src/shared/api/
  httpClient.ts
  errors.ts
  authHeader.ts
TypeScript
// src/shared/api/httpClient.ts
export async function http<T>(
  input: string,
  init?: RequestInit
): Promise<T> {
  const res = await fetch(input, {
    ...init,
    headers: {
      "content-type": "application/json",
      ...(init?.headers ?? {}),
    },
  });
 
  if (!res.ok) {
    throw new Error(`HTTP ${res.status}`);
  }
 
  return (await res.json()) as T;
}

Feature endpointi i mapiranje#

Endpointi i DTO mapiranje pripadaju feature-u jer se mijenjaju s potrebama feature-a.

TypeScript
// src/features/checkout/api/checkoutApi.ts
import { http } from "@/shared/api/httpClient";
 
export type CreateOrderInput = {
  items: Array<{ productId: string; qty: number }>;
};
 
export type CreateOrderResponse = { orderId: string };
 
export function createOrder(input: CreateOrderInput) {
  return http<CreateOrderResponse>("/api/orders", {
    method: "POST",
    body: JSON.stringify(input),
  });
}

Time izbjegavaš da centralna api mapa postane usko grlo i držiš API promjene lokalizirane.

Next.js napomena: server-only API pozivi#

Ako koristiš App Router i želiš fetchanje na serveru, premjesti server-only API module u server mapu i izbjegavaj importe u client kod.

Bash
src/features/orders/
  server/
    ordersService.ts
  api/
    types.ts

Zatim zovi ordersService iz server komponenti, route handlera ili server actions.

Za dublje obrasce oko RSC-a, vidi vodič za React Server Components.

# UI slojevi: primitive vs feature UI vs entity UI#

Tvoj UI sloj treba spriječiti dva česta problema:

  1. 1
    Copy-paste UI primitiva kroz feature-e
  2. 2
    “components” mapa koja postane odlagalište

Shared UI primitive#

To su niskorazinski gradivni blokovi: Button, Input, Modal, Tabs, Skeleton, Toast. Moraju biti:

  • stilski konzistentni
  • pristupačni po defaultu
  • s malo ovisnosti
Bash
src/shared/ui/
  button/
    Button.tsx
    Button.test.tsx
    index.ts
  input/
  modal/
  index.ts

Dobro pravilo: ako komponenta ima poslovno značenje poput CheckoutSummary, nije shared UI.

Entity UI#

Entity moduli su ponovno upotrebljivi jer predstavljaju osnovne domenske koncepte: user, product, order. Često uključuju “entity widgete” poput UserAvatar, ProductCard, OrderStatusBadge.

Bash
src/entities/product/
  model/
    types.ts
  ui/
    ProductCard.tsx
    PriceTag.tsx
  index.ts

Entity UI je “ljepilo” koje drži feature-e konzistentnima bez guranja svega u shared.

Feature UI#

Feature UI slaže shared UI i entity UI u ishode: forme, ekrane, tokove.

Bash
src/features/search/ui/
  SearchBar.tsx
  SearchResults.tsx
  SearchPage.tsx

💡 Savjet: Kad uvodiš nove shared UI primitive, odradi design review. Shared komponente su kasnije skupe za promjenu jer skupljaju mnogo konzumenata. Koristi checklistu poput React checklist za design review: performanse, pristupačnost, održivost.

# Domenska logika: drži je čistom, blizu feature-a i testabilnom#

Kad su domenska pravila razbacana po UI komponentama, dobiješ:

  • dupliciranu logiku
  • krhke promjene
  • ponašanje koje je teško testirati

Gdje domenska logika treba živjeti#

  • Pravila specifična za feature ostaju u features/<feature>/lib ili features/<feature>/model.
  • Cross-feature, stabilne domenske primitive mogu ići pod entities/<entity>/model.
  • Stvarno generički utili idu pod shared/lib.

Primjer: logika cijena u checkoutu#

TypeScript
// src/features/checkout/lib/pricing.ts
export function calcTotals(
  subtotalCents: number,
  discountCents: number,
  taxRate: number
) {
  const discounted = Math.max(0, subtotalCents - discountCents);
  const taxCents = Math.round(discounted * taxRate);
  return {
    subtotalCents,
    discountCents,
    taxCents,
    totalCents: discounted + taxCents,
  };
}

Ovo se može unit-testati bez renderiranja Reacta, što testove čini brzim i pouzdanim.

# Specifičnosti Next.js-a: App Router, RSC i granice klijenta#

Next.js dodaje ograničenja koja zapravo pomažu arhitekturi, ako ih iskoristiš.

Route datoteke samo za kompoziciju#

Drži app rute tanke:

  • čitaj parametre
  • pozovi entry komponente feature-a
  • poveži providere
TypeScript
// app/(app)/checkout/page.tsx
import { CheckoutPage } from "@/features/checkout";
 
export default function Page() {
  return <CheckoutPage />;
}

Izbjegavaj stavljati API pozive, validacijska pravila i transformacije u route datoteku.

Razdvajanje client i server koda#

Praktična konvencija:

MapaIzvršava se gdjeTipičan sadržaj
features/x/uipo defaultu clientinteraktivne komponente
features/x/serversamo serverservisi koji ovise o bazi, tajni API pozivi
features/x/apiobojeDTO tipovi, zajedničko mapiranje

Ako trebaš client komponentu, označi je eksplicitno na vrhu datoteke.

TypeScript
// src/features/checkout/ui/CheckoutForm.tsx
"use client";
 
import { useCheckout } from "../model/useCheckout";
 
export function CheckoutForm() {
  const { submit, isLoading } = useCheckout();
  return (
    <button onClick={submit} disabled={isLoading}>
      Place order
    </button>
  );
}

To čini granicu server/klijent eksplicitnom i sprječava slučajno bundlanje server-only ovisnosti u browser.

# Pravila ovisnosti koja možeš provoditi#

Pravila sprječavaju regresije kad kodna baza raste i kad dolaze novi developeri.

Praktična pravila ovisnosti#

  1. 1
    shared ne import-a ništa iz koda aplikacije.
  2. 2
    entities smije importati samo iz shared.
  3. 3
    features smije importati iz entities i shared.
  4. 4
    app smije importati iz svega, ali drugi slojevi ne bi smjeli importati iz app.
  5. 5
    Feature-i ne bi smjeli importati interne dijelove drugih feature-a. Samo javne exporte.

Primjer ESLint pravila (ograničenja importa)#

Ovo je minimalan primjer s ESLint core pravilima. Mnogi timovi dodaju i eslint-plugin-boundaries, ali čak su i core pravila dobar početak.

JavaScript
// .eslintrc.cjs
module.exports = {
  rules: {
    "no-restricted-imports": [
      "error",
      {
        patterns: [
          {
            group: ["@/features/*/*"],
            message: "Importaj samo iz javnog API-ja feature-a, ne iz internih putanja.",
          },
          {
            group: ["@/app/*"],
            message: "Ne importaj iz app sloja; app je samo za kompoziciju.",
          },
        ],
      },
    ],
  },
};

Ovo sprječava duboke importe poput @/features/checkout/ui/CheckoutForm iz drugih modula i gura developere da exportaju iz @/features/checkout.

# Strategija testiranja: što testirati i gdje to staviti#

Skalabilan setup testiranja izbjegava pretjerano oslanjanje na spore UI testove.

Preporučeno smještanje testova#

Vrsta testaLokacijaŠto pokrivaBrzo?
Unit testovifeatures/*/lib ili entities/*/modelčista logika i domenska pravilaDa
Testovi komponentishared/ui ili features/*/uiponašanje komponente, a11y, stanjaSrednje
Integracijski testovifeatures/*/teststokovi feature-a s mockanim API-jemSrednje
E2E testovie2e/stvarna korisnička putovanjaNe

Primjer: unit test za pricing#

TypeScript
// src/features/checkout/lib/pricing.test.ts
import { calcTotals } from "./pricing";
 
test("calcTotals computes total with discount and tax", () => {
  const r = calcTotals(10000, 2000, 0.25);
  expect(r.totalCents).toBe(10000);
  expect(r.taxCents).toBe(2000);
});

Drži unit testove blizu logike. Kad refaktoriraš, testovi se sele s kodom i ostaju lako pronađivi.

# Česte skalabilnosne zamke i kako ih izbjeći#

Zamka 1: shared mapa postane “ladica za svašta”#

Ako shared uključuje kod specifičan za feature, svi počnu ovisiti o njemu i refaktori postaju rizični.

Rješenje: shared sadrži samo primitive i stvarno generičke libove. Poslovno značenje premjesti u entities ili features.

Zamka 2: “components” kao jedini organizacijski koncept#

Top-level components mapa skriva poslovni kontekst. Završit ćeš tako da tražiš “gdje je checkout forma” i pogađaš.

Rješenje: feature moduli posjeduju svoj UI. Shared UI su samo primitive.

Zamka 3: Duboki importi koji zaobilaze granice#

Duboki importi su vodeći indikator budućeg sprezanja (coupling).

Rješenje: provodi javne API-je kroz index.ts datoteke i lint pravila.

Zamka 4: Duplicirana logika mapiranja API-ja#

Kad svaki ekran drugačije mapira isti DTO, bugovi se pojavljuju pri promjenama API-ja.

Rješenje: mapiraj DTO-e jednom unutar feature api modula ili entity model mappera.

# Održivi primjer: kako sve povezati#

Pretpostavimo da gradiš e-commerce aplikaciju s pretragom, checkoutom i računom.

  • shared/ui daje Button, Input, Dialog.
  • entities/product daje ProductCard, tipove i formatiranje.
  • features/search posjeduje stanje upita pretrage, debouncing, search API i UI rezultata.
  • features/checkout posjeduje pricing logiku, slanje košarice i payment flow.
  • app spaja routing, providere i kompoziciju feature-a.

Ovo drži promjene lokalnima:

  • Promjena pricing pravila utječe na features/checkout/lib/pricing.ts i testove.
  • Promjena načina prikaza proizvoda utječe na entities/product/ui/ProductCard.tsx.
  • Promjena Button stylinga utječe na shared/ui/button.

Kad radiš arhitekturni review, možeš brzo skenirati granice feature-a, a zatim primijeniti checklistu poput React checklist za design review: performanse, pristupačnost, održivost.

# Ključne poruke#

  • Organiziraj po feature-ima i entitetima, ne po generičkim “components”, i route datoteke drži tankima kao kompoziciju.
  • Stavi tanak HTTP transport u shared/api, a endpoint-e i DTO mapiranje drži unutar svakog feature-a.
  • Razdvoji UI na shared primitive, entity UI i feature UI kako bi izbjegao napuhan shared sloj.
  • Provodi jednosmjerne ovisnosti i javne API-je feature-a koristeći index.ts exporte i ESLint ograničenja importa.
  • Drži domensku logiku čistom i testiraj je brzim unit testovima blizu koda, a zatim dodaj integracijske i E2E samo gdje je potrebno.

# Zaključak#

Feature-based React frontend arhitektura uspijeva kad su granice provedive, importi namjerni, a poslovna logika ostaje blizu feature-a koji je posjeduje. Ako želiš pomoć pri primjeni ove strukture na postojeću kodnu bazu ili ti treba Next.js App Router arhitektura spremna za RSC, pristupačnost i performance review, javi se Samiodi za arhitekturni audit i konkretan plan refaktora.

FAQ

Share
A
Adrijan OmićevićOsnivač i senior developer

Osnivač i senior developer u Samiodi. 8+ godina iskustva u izradi React, Next.js, Flutter i n8n rješenja za klijente diljem Europe.

Trebate pomoć s projektom?

Gradimo prilagođena rješenja koristeći tehnologije iz ovog članka. Senior tim, fiksne cijene.