Siirry sisältöön
Takaisin artikkeleihin

Next.js App Routerin projektirakenne: päätökset, joita on kallista muuttaa

Next.js App Routerille ei ole virallista suositeltua kansiorakennetta. Dokumentaatio listaa vaihtoehdot ja jättää valinnan sinulle, mikä on rehellistä ja täysin hyödytöntä siinä vaiheessa kun repo on tyhjä.

aleksi huuskon profiilikuva

Kirjoittanut Aleksi Huusko

Julkaistu 30.7.2026

Next.js App Routerin projektirakenne

Suurin osa kansiorakennekiistoista koskee asioita, jotka voi nimetä uudelleen yhdessä iltapäivässä.

Muutama päätös taas on kantava rakenne, ja ne kannattaa miettiä ennen kuin sitoutuu. Alla oleva rakenne on kaksikielisen tuotantosivuston takaa, CMS mukana, joten ne kohdat jotka merkitsen kivuliaiksi ovat oikeasti olleet kivuliaita.

Lyhyt versio

apps/web/src/
  app/
    [lang]/
      (site)/         ## markkinointisivut, navi ja footer mukana
      (funnel)/       ## paljaat laskeutumissivut, ei kehyksiä
      layout.tsx      ## oikea juuri leiskat
    api/
    fonts/
  components/
    sections/         ## sivutason lohkot, yksi per CMS-lohkotyyppi
    elements/         ## small pienet jaetut palaset
  lib/                ## puhdas logiikka, ei JSX:ää
  hooks/
  proxy.ts

Neljä ylätason kansiota src:n alla. Jos kaipaat viidettä, kaipaat todennäköisesti pakettia.

Reittiryhmät kannattaa päättää aikaisin

Reittiryhmät ovat niitä sulkeisiin käärittyjä kansioita. Ne eivät näy osoitteessa. Ne ovat olemassa siksi, että kaksi sivujoukkoa voi käyttää eri layoutia.

Jako joka maksaa itsensä takaisin kulkee siinä, onko sivulla sivuston kehykset vai ei. Markkinointisivut haluavat navin ja footerin. Maksetun kampanjan tai liidimagneetin laskeutumissivu ei halua kumpaakaan, koska jokainen navin linkki on paikka johon kävijä voi poistua.

Kun nämä laitetaan (site)- ja (funnel)-ryhmiin, ero on yksi tiedosto eikä ehtolause jaetussa layoutissa, joka kasvattaa uuden haaran joka kvartaali.

Päätä tämä aikaisin. Reitin siirtäminen ryhmästä toiseen vaihtaa sen layoutin joka kääritään reitin ympärille, ja layouteissa asuvat providerit.

Juuri leiskat ei välttämättä ole juuressa

Jos lokalisoit dynaamisella segmentillä, app/[lang]/layout.tsx on oikea juurilayoutisi. app/layout.tsx-tiedostoa ei ole lainkaan, ja <html>- ja <body>-tagit istuvat tason alempana.

Seuraus näkyy tuotannossa eikä kehityksessä. Pyyntö polkuun jota ei ole olemassa ei osu mihinkään kielisegmenttiin, joten se ei koskaan päädy layoutiisi, ja kävijä saa frameworkin sisäänrakennetun 404-sivun sen sijaan jonka itse suunnittelit.

app/layout.tsx:n lisääminen ei korjaa tätä. Kahta juurilayoutia ei voi olla, ja sellaisen lisääminen rikkoo sen tapauksen joka jo toimi. Tämä on kielietuliitteisen reitityksen aito kompromissi, ja käytännön vastaus on huolehtia siitä etteivät omat linkkisi koskaan johda 404:ään, sen sijaan että käyttäisi iltapäivän sellaisen sivun tyylittelyyn jolle kenenkään ei pitäisi päätyä. Tämä kannattaa silti tietää ennen kuin valitsee mallin.

Jaa komponentit sen mukaan miten niitä käytetään

components/sections/ sisältää sivutason lohkot: täysleveitä osioita jotka omistavat oman pystysuuntaisen välistyksensä ja jotka page builder kokoaa sivuksi. Yksi tiedosto per CMS-lohkotyyppi, joten sisällöntuottajan lisäämä FAQ haitari vastaa täsmälleen yhtä tiedostoa.

components/elements/ sisältää ne pienet palaset joista osiot rakentuvat. Napit, logo, kielenvaihtaja, evästeilmoitus.

Raja toimii, koska se vastaa kysymykseen jonka joku oikeasti esittää ääneen: voiko sisällöntuottaja pudottaa tämän sivulle yksinään? Kyllä tarkoittaa osiota, ei tarkoittaa elementtiä. Vertaa tätä atomeihin, molekyyleihin ja organismeihin, joissa luokittelu on tulkintakysymys jota kukaan ei ratkaise kahdesti samalla tavalla.

`lib/` on logiikalle joka ei importtaa Reactia

Kielitaulukot, sanakirjat, SEO-metadatan rakentajat, URL-apurit. Jos tiedosto tarvitsee JSX:ää se on komponentti, jos se tarvitsee useState:ia se on hook, ja kaikki muu asuu lib/:ssä.

Testi jota itse käytän on se, voisiko asian yksikkötestata ilman rendereriä. Se estää lib/:iä muuttumasta laatikoksi johon luokittelemattomat tiedostot menevät kuolemaan.

Kaksi tiedostoa on täysin hyvä koko hooks/-kansiolle. Sitä ei tarvitse täyttää.

Middleware on nyt nimeltään `proxy`

Next.js 16 nimesi middleware.ts:n uudelleen proxy.ts:ksi, ja ulos vietävän funktion on myös oltava nimeltään proxy. Jos seuraat sitä ennen kirjoitettua ohjetta, uudelleenohjauksesi eivät aja koskaan eikä mikään kerro siitä, mikä on hauska iltapäivä.

Kohta josta kannattaa oikeasti olla tarkkana on matcher. Se on staattinen merkkijono, ja jokainen ylätason polku joka ei ole kieli pitää sulkea pois käsin. Jos yksi unohtuu, se ohjautuu kielietuliitteen taakse jossa ei ole reittiä. Oman domainin kautta ohjattu analytiikkaliikenne on klassinen uhri, koska se on ylätason polku joka näyttää sivulta muttei ole sellainen.

Kirjoita poissulkulista kerran ja täydennä sitä joka kerta kun lisäät ylätason reitin. Se on repon vähiten näppärä tiedosto ja se rikkoo mielellään jotain mitä et huomaa viikkoon.

Tyypitetyt reitit ja niiden rajat

Tyypitetyt reitit tekevät href:istä tarkistetun tyypin, joten linkki poistettuun reittiin muuttuu build-virheeksi. Se on hyödyllistä. Se on myös kapeampaa kuin useimmat odottavat kytkiessään sen päälle.

Dynaamiset segmentit levenevät string:iksi. Reitti kuten /blog/[slug] hyväksyy minkä tahansa merkkijonon, koska tyypitetyt reitit varmistavat että reitti on olemassa, eivät sitä että slug osoittaa oikeaan sisältöön. Linkki blogipostaukseen jonka poistit viime kuussa menee yhä tyyppitarkistuksesta läpi.

Kaikki mikä rakennetaan merkkijonoja yhdistelemällä on sille näkymätöntä. Sillä hetkellä kun kokoat polun kielimuuttujasta ja slugista, pitelet taas tavallista merkkijonoa, ja kielietuliitteinen reititys tekee juuri tuota jatkuvasti. Ulkoiset linkit ja CMS:ssä kirjoitetut linkit eivät saa mitään apua, ja sisältövetoisella sivustolla juuri niistä rikkinäiset linkit tulevat.

Se siis nappaa ne refaktoroinnit jotka teit tarkoituksella ja ohittaa sen sisällön joka lahosi itsestään. Kohtele sitä uudelleennimeämisen turvaverkkona eikä linkkien validointina, ja pidä oikeat uudelleenohjaukset jälkimmäistä ongelmaa varten.

Milloin kannattaa tarttua monorepoon

Yksi sivusto ei tarvitse sitä. Merkki on toinen kuluttaja koodillesi.

Täällä se ansaitsee paikkansa siksi, että CMS Studio on erillinen sovellus joka jakaa tyypit ja kyselyt sivuston kanssa, ja jakaminen on juuri se mitä varten paketti on. Testi on se, tarvitseeko jokin apps/web:n ulkopuolella sitä. Jos ei mikään tarvitse, kyseessä on kansio.

Yksi sääntö kannattaa pitää voimassa ensimmäisestä päivästä: paketilla pitää olla eksplisiittinen lista siitä mitä siitä voi importata, eikä barrel-tiedostoa joka vie kaiken uudelleen ulos. Barrel-tiedostot tekevät mahdottomaksi nähdä mikä riippuu mistäkin, mikä on juuri se ongelma jonka monorepon piti ratkaista.

Mitä jätän tekemättä

Kansio per reitti, jossa komponentit ovat reitin vieressä. Olen kokeillut. Suurin osa komponenteista päätyy käyttöön kahdesti, ja sitten ne siirtyvät kuitenkin.

types/-kansio. Tyypit kuuluvat sen viereen mitä ne kuvaavat, ja CMS-skeemasta johdetut tyypit kuuluvat yhteen generoituun tiedostoon jota et koskaan muokkaa käsin.

Sekä utils/ että helpers/. Valitse toinen sana ja pysy siinä.

constants/-kansio. Vakiot asuvat sen koodin vieressä joka lukee niitä.

Mitä tästä jää käteen

Panosta reittiryhmiin ja lokalisoinnin muotoon, koska ne ovat rakenteellisia ja niiden peruminen tarkoittaa jokaiseen layoutiin koskemista. Sen jälkeen valitse mikä tahansa järkevä komponenttijako ja lakkaa miettimästä sitä. Se on pelkkä uudelleennimeäminen.

Merkki siitä että rakenne toimii on se, että uudella tiedostolla on ilmeinen koti. Jos joudut pysähtymään miettimään mihin jokin kuuluu, raja on todennäköisesti väärässä paikassa, eikä yksi lisätaso sisäkkäisyyttä pelasta sitä.

Lue seuraavaksi