Frontend arkitektur for store projekter — Sådan skalerer du din kodebase
Når du arbejder på et frontend-projekt med 2-3 udviklere, kan du slippe afsted med mange ting. Mappestruktur? Det løser sig selv. State management? Props er fint. Kodekonventioner? Vi snakker bare sammen.
Men når teamet vokser til 10, 20 eller 50 udviklere, og kodebasen rammer hundredtusinder af linjer kode, så falder den tilgang fra hinanden. Pludselig er der konflikter i hver merge request, ingen kan finde noget, og performance er gået i stændig tilbagegang.
Jeg har arbejdet med frontend i enterprise-skala hos Grundfos, Vestas, Playable og Harness — og nu som Technical Director hos Checkmate.dk. Her er de vigtigste lærdomme om hvordan du arkitekterer frontend-applikationer der faktisk skalerer.
Komponentarkitektur og mappestruktur
Den første og mest grundlæggende beslutning er hvordan du organiserer dine komponenter. Jeg har set to primære tilgange:
Feature-baseret struktur er næsten altid det rigtige valg for store projekter. I stedet for at gruppere efter type (alle buttons i en mappe, alle modals i en anden), grupperer du efter feature:
components/
Dashboard/
DashboardView.vue
DashboardStats.vue
DashboardChart.vue
useDashboardData.ts
UserProfile/
UserProfileView.vue
UserProfileForm.vue
useUserProfile.ts
UI/
BaseButton.vue
BaseModal.vue
BaseInput.vue
Grunden er simpel: når en udvikler arbejder på en feature, skal de kun røre filer i en mappe. Det reducerer merge-konflikter drastisk og gør det nemmere at forstå konteksten.
UI-komponenter (buttons, inputs, modals) lever for sig selv i en delt mappe, fordi de bruges på tværs af features. Men feature-specifikke komponenter hører til i feature-mappen — ikke i en generisk components/ rod.
En tommelfingerregel jeg bruger: Hvis en komponent kun bruges et sted, hører den til i den features mappe. Først når den bruges to eller flere steder, flyttes den til en delt mappe.
State management — Pinia vs composables vs props
Et af de mest debatterede emner i frontend-verdenen. I Vue-økosystemet har vi Pinia, composables og props. I React er det Redux/Zustand, hooks og props. Men principperne er de samme.
Props er førstevalget. Altid. Data der flyder fra forælder til barn er det simpleste og mest forudsigelige mønster. Når folk klager over "prop drilling", er det ofte et tegn på at komponenthierarkiet er forkert — ikke at du mangler global state.
Composables (eller custom hooks) er næste trin. Når logik skal deles mellem komponenter der ikke har et direkte forælder-barn-forhold, er en composable perfekt. Den indkapsler logik og state uden at gøre det globalt.
// useCurrentUser.ts
export function useCurrentUser() {
const user = ref<User | null>(null)
const isLoading = ref(false)
async function fetchUser() {
isLoading.value = true
user.value = await api.getCurrentUser()
isLoading.value = false
}
return { user, isLoading, fetchUser }
}
Pinia/Redux er kun til ægte global state — ting som den aktuelle bruger, tema-indstillinger, notifikationer. Hvis du har mere end 10-15 stores, har du sandsynligvis lagt for meget i global state.
Min erfaring fra Harness, hvor vi havde en stor Vue-applikation: Teamet startede med at lægge alt i Vuex (forgængeren til Pinia). Resultatet var en massiv, uforudsigelig state-graf hvor ingen vidste hvad der påvirkede hvad. Da vi refaktorerede til composables for feature-specifik state og kun beholdt ægte global state i stores, blev kodebasen markant nemmere at arbejde med.
Code splitting og lazy loading
I en stor applikation er det afgørende at brugeren ikke skal downloade hele din kodebase for at se første side. Både Vue Router og React Router understøtter lazy loading af routes ud af boksen:
// Vue Router
const routes = [
{
path: '/dashboard',
component: () => import('./pages/Dashboard.vue')
},
{
path: '/settings',
component: () => import('./pages/Settings.vue')
}
]
Men routes er bare begyndelsen. I store projekter bør du også overveje:
- Lazy loading af tunge komponenter — Diagrammer, editorer, og lignende bør loades on-demand
- Prefetching af sandsynlige næste sider — Hvis 80% af brugerne går fra dashboard til rapporter, så prefetch rapporter-chunken
- Dynamiske imports for tredjepartsbiblioteker — Lad være med at bundle moment.js eller lodash ind i din hovedbundle hvis det kun bruges et sted
Den største fejl jeg ser er at teams optimerer for tidligt. Start med route-baseret code splitting. Mål din bundle-størrelse. Optimer derefter de største chunks først.
Design system integration
Når du har mere end 3-4 frontend-udviklere, har du brug for et design system. Ikke et fancy Storybook-setup med 200 komponenter — men en fælles forståelse af de basale byggeklodser.
Et effektivt design system i praksis:
- Tokens først — Definer farver, spacing, typografi og breakpoints som design tokens (CSS custom properties eller en theme-fil). Alt andet bygger på disse.
- Primitive komponenter — Button, Input, Select, Modal, Card. Maks 15-20 komponenter. De skal være veltestede og veldokumenterede.
- Kompositionsmønstret — Byg komplekse komponenter ved at kombinere primitive. Lad være med at lave en
SuperTableder kan alt — lav i stedetTable,TableHeader,TableRow,TableCellder kan sammensættes.
Det vigtigste er at design systemet lever tæt på koden. Jeg har set for mange teams hvor design systemet er et separat projekt der altid er bagud. Når det lever i en delt pakke i din monorepo (eller som en npm-pakke med hurtig release-cyklus), bliver det faktisk brugt.
Monorepo vs polyrepo
Det her er en beslutning der har store konsekvenser, og der er ikke et universelt svar.
Monorepo (alle pakker i et repository) er bedst når:
- Teams deler meget kode (design system, utilities, typer)
- Du vil have atomare ændringer på tværs af pakker
- Du har værktøj til det (Nx, Turborepo, eller Lerna)
Polyrepo (separate repositories) er bedst når:
- Teams er meget autonome og sjældent deler kode
- Du har strikse deployment-boundaries
- Teams bruger forskellige tech stacks
Min erfaring: For de fleste frontend-teams er en monorepo det rigtige valg. Værktøjerne (særligt Nx og Turborepo) er blevet så modne at overhead er minimal, og fordelen ved at kunne dele typer, konfiguration og design system-komponenter er enorm.
Hos Vestas arbejdede vi med en polyrepo-tilgang der betød at vi konstant kæmpede med at holde delte biblioteker i sync. Hos Playable skiftede vi til monorepo, og det var en game-changer for developer experience.
TypeScript som skaleringsværktøj
TypeScript er ikke bare "JavaScript med typer." I store projekter er det et arkitekturværktøj.
Interfaces som kontrakter. Når team A bygger en API og team B bruger den, fungerer TypeScript-interfaces som en levende kontrakt. Hvis API'en ændrer sig, får team B en kompileringsfejl — ikke en runtime-fejl i produktion kl. 3 om natten.
Strenge typer for domæneobjekter. Definer dine domæneobjekter som TypeScript-typer og brug dem konsekvent:
interface Order {
id: string
status: 'pending' | 'confirmed' | 'shipped' | 'delivered'
items: OrderItem[]
customer: Customer
createdAt: Date
}
Når status er en union type i stedet for en string, kan du ikke ved et uheld sætte den til "PENDING" eller "Shipped". Det lyder som en lille ting, men i en kodebase med 50 udviklere forhindrer det hundredevis af bugs.
Generics for genbrugelig kode. Composables og utility-funktioner der bruger generics er mærkbart nemmere at genbruge korrekt i store teams.
Teststrategi i skala
Store projekter har brug for en strategi — ikke bare "vi skriver tests." Her er den pyramide jeg anbefaler:
Unit tests (70%) — Test composables, utility-funktioner og forretningslogik. De er hurtige, stabile og giver mest værdi per tidsenhed.
Komponent-tests (20%) — Test at komponenter renderer korrekt med forskellige props og håndterer brugerinteraktion. Brug Vue Test Utils eller React Testing Library.
End-to-end tests (10%) — Test kritiske brugerflows (login, checkout, osv.) med Playwright eller Cypress. Hold antallet lavt — de er langsomme og skrøbelige.
Den største fejl er at starte med E2E-tests. De er dyre at vedligeholde og giver falsk tryghed. Start med unit tests af din forretningslogik, og tilføj E2E-tests for de mest kritiske flows.
Teamkonventioner og code review
Værktøj og arkitektur er kun halvdelen. Den anden halvdel er mennesker og processer.
Automatiser hvad der kan automatiseres. ESLint, Prettier, husky pre-commit hooks. Diskuter ikke formatering i code reviews — lad værktøjerne håndtere det.
Brug ADR'er (Architecture Decision Records). Når teamet træffer en større beslutning (f.eks. "vi bruger Pinia i stedet for composables til global state"), skriv det ned med kontekst og begrundelse. Nye teammedlemmer vil takke dig.
Code reviews er læring, ikke gatekeeping. De bedste teams jeg har arbejdet med bruger code reviews som en mulighed for videndeling. Forklar hvorfor du foreslår en ændring, ikke bare hvad. Og vær åben for at lære noget nyt fra den der har skrevet koden.
Definer "done" for komponenter. En komponent er ikke færdig når den virker. Den er færdig når den har typer, er testet, er dokumenteret (mindst en kommentar med et eksempel), og er reviewet.
Afslutningstanker
God frontend-arkitektur handler ikke om at vælge det rigtige framework eller det nyeste værktøj. Det handler om at skabe struktur der gør det muligt for mange mennesker at arbejde effektivt på den samme kodebase over lang tid.
Start simpelt. Tilføj kompleksitet når du har bevist at du har brug for det. Og husk: den bedste arkitektur er den dit team faktisk forstår og følger.
Har du brug for hjælp med at skalere din frontend-arkitektur? Jeg arbejder til dagligt med disse udfordringer som Technical Director hos Checkmate.dk, og jeg deler regelmæssigt mine erfaringer her på bloggen. Kontakt mig hvis du vil diskutere dit projekt, eller følg med her for flere artikler om frontend i enterprise-skala.
Har du brug for en erfaren udvikler?
Lad os tage en snak om hvordan jeg kan hjælpe med teknisk strategi, arkitektur og frontend-udvikling.
Kontakt mig
Nicky Christensen