modul 00 / ce este Next.js & de ce există
React vs Next.js: problema pe care o rezolvă
Next.js e cel mai folosit framework React pentru aplicații de producție. Acest curs urmează structura curriculumului oficial Next.js Learn (Vercel) și se concentrează pe App Router — abordarea modernă, recomandată.
ce îți lipsește cu React „gol"
React e o bibliotecă pentru interfețe, nu un framework complet. Îți dă componente și stare, dar te lasă singur cu deciziile mari:
- Rutare — React nu are rutare; instalezi și configurezi ceva separat.
- Randare pe server — o aplicație React clasică trimite un HTML gol și abia apoi JavaScript-ul construiește pagina. Prost pentru SEO și pentru prima afișare.
- Data fetching — trebuie să gândești singur unde și când aduci datele, cu ce cache.
- Bundling, optimizări, imagini, fonturi — le configurezi manual.
Next.js le rezolvă pe toate, cu decizii bune „din cutie": rutare bazată pe fișiere, randare pe server implicit, optimizări automate, un singur proiect pentru front-end și back-end.
modurile de randare — conceptul central
| Mod | Când se generează HTML-ul | Bun pentru |
| CSR (client-side) | În browser, după ce se încarcă JS | Dashboard-uri private, interactivitate grea |
| SSR (server-side) | La fiecare cerere, pe server | Conținut personalizat sau mereu proaspăt |
| SSG (static) | La build, o dată | Blog, docs, marketing — rapid și ieftin |
| ISR (incremental) | Static, dar regenerat periodic | Conținut care se schimbă rar, dar se schimbă |
Puterea Next.js: poți alege modul per pagină, chiar per bucată de pagină. Nu ești blocat într-o singură strategie pentru tot site-ul — o pagină de blog poate fi statică, iar coșul de cumpărături dinamic, în aceeași aplicație.
de ce contează randarea pe server
- SEO — motoarele de căutare primesc HTML cu conținut real, nu o pagină goală.
- Viteză percepută — utilizatorul vede conținut imediat, nu un spinner.
- Dispozitive slabe — munca se face pe server, nu pe telefonul utilizatorului.
de reținut
React e o bibliotecă de interfețe; Next.js e framework-ul care adaugă tot ce lipsește pentru producție: rutare, randare pe server, data fetching, optimizări. Ideea cheie de reținut e că poți alege modul de randare per pagină (static, la cerere, regenerat) — în loc să tratezi tot site-ul la fel. Restul cursului e, în esență, învățarea uneltelor prin care faci această alegere conștient.
modul 00 / ce este Next.js & de ce există
structura unui proiect App Router
Un proiect Next.js modern se creează cu o singură comandă și are o structură bine definită. Înțelegerea folderelor te scutește de multă confuzie.
crearea proiectului
npx create-next-app@latest numele-proiectului
Te întreabă câteva lucruri (TypeScript, Tailwind, App Router) și generează un proiect gata configurat. Apoi:
cd numele-proiectului
npm run dev # server de dezvoltare, http://localhost:3000
structura de bază
proiect/
app/ # rutele & UI-ul (App Router)
layout.tsx # învelișul comun (obligatoriu, rădăcină)
page.tsx # pagina „/"
globals.css
public/ # fișiere statice (imagini, favicon)
next.config.js # configurarea framework-ului
package.json
| Element | Rol |
| app/ | Inima aplicației — fiecare folder devine un segment de rută |
| page.tsx | Face un folder accesibil public ca pagină |
| layout.tsx | Înveliș comun (header, footer) partajat de paginile din acel folder |
| public/ | Servit ca atare la rădăcină: public/logo.png → /logo.png |
App Router vs Pages Router
Vei întâlni în tutoriale două sisteme. Pages Router (folderul pages/) e vechiul sistem, încă suportat. App Router (folderul app/) e cel modern și recomandat — aduce Server Components, layouts imbricate, streaming. Acest curs folosește App Router. Când citești documentație sau răspunsuri online, verifică mereu despre care e vorba, altfel amesteci două lumi.
prima pagină
// app/page.tsx
export default function Home() {
return <h1>Salut din Next.js!</h1>;
}
Un fișier page.tsx care exportă implicit o componentă React = o pagină. Fără configurări, fără înregistrări de rute.
de reținut
Creezi proiectul cu create-next-app și lucrezi în folderul app/: fiecare folder e un segment de rută, page.tsx îl face pagină, layout.tsx îi dă un înveliș comun. Atenție la distincția App Router (modern, folderul app/ — ce învățăm aici) vs Pages Router (vechi, pages/) când citești resurse online. Fișierele speciale sunt convenția pe care se sprijină tot framework-ul.
modul 01 / rutare bazată pe fișiere
rute, rute imbricate & rute dinamice
În Next.js nu configurezi rute — le creezi făcând foldere. Structura folderelor din app/ ESTE harta URL-urilor. E una dintre cele mai plăcute idei ale framework-ului.
reguli de bază
| Fișier / folder | URL rezultat |
| app/page.tsx | / |
| app/despre/page.tsx | /despre |
| app/blog/page.tsx | /blog |
| app/blog/categorii/page.tsx | /blog/categorii |
Regula de aur: un folder devine un segment de URL, dar devine accesibil doar dacă are un page.tsx. Un folder fără page.tsx e doar organizare (poate ține componente, layout-uri, utilitare) — nu e o rută publică.
rute dinamice: [param]
Pentru pagini care depind de o valoare (un articol, un produs), folosești un folder cu paranteze drepte:
app/blog/[slug]/page.tsx → /blog/orice-articol
Valoarea ajunge în componentă ca params:
// app/blog/[slug]/page.tsx
export default async function Articol({ params }) {
const { slug } = await params;
return <h1>Articol: {slug}</h1>;
}
variante de segmente dinamice
| Sintaxă | Prinde |
| [slug] | Un singur segment: /blog/salut |
| [...slug] | Toate segmentele următoare: /docs/a/b/c |
| [[...slug]] | La fel, dar opțional (prinde și /docs) |
grupuri de rute: (folder)
Un folder în paranteze rotunde organizează fișierele fără a apărea în URL:
app/(marketing)/despre/page.tsx → /despre (nu /marketing/despre)
Util ca să grupezi secțiuni care împart un layout (ex: toate paginile de marketing cu un header, cele de aplicație cu altul), fără a strica URL-urile.
de reținut
Rutarea e structura folderelor: un folder = un segment, iar page.tsx îl face public. Foldere în [paranteze drepte] = segmente dinamice (valoarea vine în params); în (paranteze rotunde) = grupare care nu apare în URL. Odată internalizate aceste trei convenții, poți citi arhitectura oricărei aplicații Next.js doar uitându-te la foldere.
modul 01 / rutare bazată pe fișiere
navigarea: Link & hooks de rutare
Într-o aplicație Next.js nu folosești <a href> pentru navigarea internă — folosești componenta Link, care aduce navigare instantă, fără reîncărcarea paginii.
componenta Link
import Link from 'next/link';
export default function Nav() {
return (
<nav>
<Link href="/">Acasă</Link>
<Link href="/blog">Blog</Link>
<Link href={`/blog/${'{'}slug{'}'}`}>Articol</Link>
</nav>
);
}
de ce Link și nu <a>
- Navigare client-side — se schimbă doar ce trebuie, fără reîncărcarea completă a paginii.
- Prefetch automat — Next.js pre-încarcă în fundal paginile ale căror linkuri sunt vizibile pe ecran, deci click-ul pare instant.
- Păstrează starea layout-urilor (ex: un player audio în layout continuă să cânte între pagini).
Pentru linkuri externe folosești normal <a href="https://..."> — Link e pentru rutele proprii.
hooks de navigare (doar în Client Components)
| Hook | Ce face |
| usePathname() | Îți dă calea curentă (ex: „/blog") — util pentru a marca linkul activ |
| useRouter() | Navigare programatică: router.push('/dashboard') |
| useSearchParams() | Citește parametrii din query string (?q=ceva) |
'use client'; // hooks-urile cer Client Component
import { usePathname } from 'next/navigation';
export default function NavLink({ href, children }) {
const cale = usePathname();
const activ = cale === href;
return <Link href={href} className={activ ? 'activ' : ''}>{children}</Link>;
}
Observă 'use client' — hooks-urile rulează în browser, deci componenta trebuie să fie Client Component. E conceptul central al modulului 3.
de reținut
Pentru navigarea internă folosești <Link>, nu <a>: aduce navigare client-side, prefetch automat și păstrarea stării layout-urilor. Pentru navigare programatică sau pentru a ști unde ești, ai hooks-urile useRouter, usePathname, useSearchParams — dar acestea rulează în browser, deci cer 'use client'. Linkurile externe rămân <a> obișnuit.
modul 02 / layouts, fișiere speciale & metadata
layouts imbricate & template
Layout-urile sunt una dintre marile îmbunătățiri ale App Router. Un layout e un înveliș comun pentru paginile dintr-un segment — și, crucial, nu se re-randează la navigarea între paginile lui.
layout-ul rădăcină (obligatoriu)
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html lang="ro">
<body>
<Header />
{children}
<Footer />
</body>
</html>
);
}
Layout-ul rădăcină e obligatoriu și e singurul care conține tag-urile <html> și <body>. children e pagina (sau layout-ul copil) care se randează înăuntru.
layout-uri imbricate
Orice folder poate avea propriul layout.tsx, care se cuibărește în cel părinte:
app/layout.tsx → header + footer global
app/dashboard/layout.tsx → + un meniu lateral
app/dashboard/setari/page.tsx
Rezultat: header → meniu lateral → pagina de setări
de ce contează: starea se păstrează
La navigarea între /dashboard/setari și /dashboard/profil, layout-ul dashboard-ului rămâne montat — nu se re-randează. Practic:
- Scroll-ul dintr-un meniu lateral lung nu sare la început.
- O stare din layout (un filtru deschis, un player) supraviețuiește navigării.
- Se re-randează doar partea care chiar s-a schimbat — navigare mai rapidă.
Dacă vrei opusul (o componentă care se resetează la fiecare navigare — ex: o animație de intrare), folosești template.tsx în loc de layout.tsx: e la fel, dar se re-montează la fiecare schimbare de rută.
de reținut
layout.tsx = înveliș partajat care rămâne montat între navigările din segmentul lui — de aici vin navigarea rapidă și starea păstrată (scroll, filtre, playere). Layout-ul rădăcină e obligatoriu și deține <html>/<body>. Se pot cuibări oricâte niveluri. Dacă ai nevoie de re-montare la fiecare navigare, folosești template.tsx.
modul 02 / layouts, fișiere speciale & metadata
loading, error, not-found & metadata
App Router folosește fișiere speciale cu nume rezervate: le creezi într-un folder și framework-ul le folosește automat. Nu configurezi nimic.
fișierele speciale principale
| Fișier | Rol |
| page.tsx | Pagina propriu-zisă |
| layout.tsx | Înveliș partajat, persistent |
| loading.tsx | UI afișat automat cât se încarcă pagina (Suspense sub capotă) |
| error.tsx | UI afișat dacă apare o eroare în acel segment (Client Component) |
| not-found.tsx | UI pentru 404 (declanșat și manual cu notFound()) |
| route.ts | Endpoint de API în loc de pagină (modulul 8) |
loading.tsx — UI de încărcare gratuit
// app/dashboard/loading.tsx
export default function Loading() {
return <p>Se încarcă dashboard-ul…</p>;
}
Doar creând acest fișier, Next.js afișează automat conținutul lui cât timp pagina își aduce datele. Sub capotă înfășoară segmentul într-un Suspense (modulul 6). Zero cod de „isLoading" scris de mână.
error.tsx — izolarea erorilor
'use client'; // error.tsx e MEREU Client Component
export default function Error({ error, reset }) {
return (
<div>
<h2>Ceva n-a mers</h2>
<button onClick={reset}>Încearcă din nou</button>
</div>
);
}
O eroare într-un segment afectează doar acel segment — restul aplicației (layout, navigație) rămâne funcțional. reset() încearcă re-randarea.
metadata — SEO din cod
Fiecare pagină sau layout poate exporta un obiect metadata, iar Next.js generează tag-urile din <head>:
export const metadata = {
title: 'Blog · Studio Academy',
description: 'Articole despre dezvoltare web.',
openGraph: { images: ['/og/cover.png'] },
};
Pentru pagini dinamice (unde titlul depinde de date), folosești varianta funcție:
export async function generateMetadata({ params }) {
const { slug } = await params;
const articol = await getArticol(slug);
return { title: articol.titlu, description: articol.rezumat };
}
de reținut
App Router îți dă comportamente întregi doar prin crearea unui fișier cu nume rezervat: loading.tsx (stare de încărcare automată), error.tsx (erori izolate pe segment, cu reset), not-found.tsx (404). Pentru SEO exporți metadata (static) sau generateMetadata (dinamic, pe baza datelor). Aceste convenții înlocuiesc mult cod pe care în React „gol" l-ai scrie manual.
modul 03 / Server Components vs Client Components
modelul mental: server implicit
Acesta e cel mai important modul al cursului. Dacă înțelegi bine distincția Server/Client Components, tot restul App Router devine logic. Dacă n-o înțelegi, totul pare capricios.
regula fundamentală
În App Router, fiecare componentă e Server Component implicit. Rulează pe server, iar în browser ajunge doar HTML-ul rezultat — nu și codul componentei. Devine Client Component doar dacă scrii 'use client' în capul fișierului.
// Server Component (implicit — nimic de scris)
export default async function Produse() {
const produse = await db.produse.findMany(); // direct în DB!
return <ul>{produse.map(p => <li key={p.id}>{p.nume}</li>)}</ul>;
}
ce câștigi cu Server Components
- Acces direct la resurse de server — baza de date, fișiere, API-uri interne, chei secrete. Nu construiești un API doar ca să-ți aduci propriile date.
- Bundle mai mic — codul componentei nu se trimite în browser. O bibliotecă grea de formatare folosită doar pe server nu costă nimic utilizatorului.
- Async direct — poți scrie async function și await chiar în componentă.
- Secretele rămân secrete — cheile API folosite într-un Server Component nu ajung niciodată în browser.
ce NU poate un Server Component
| Nu poate | Pentru că |
| useState, useReducer | Nu are stare în browser |
| useEffect | Nu rulează în browser |
| onClick, onChange | Evenimentele sunt în browser |
| window, localStorage | Nu există pe server |
când ai nevoie de Client Component
Pui 'use client' când componenta are nevoie de interactivitate sau API-uri de browser:
'use client';
import { useState } from 'react';
export default function Contor() {
const [n, setN] = useState(0);
return <button onClick={() => setN(n + 1)}>Ai dat clic de {n} ori</button>;
}
de reținut
Regula de aur: totul e Server Component implicit; treci la Client Component doar când ai nevoie de stare, efecte, evenimente sau API-uri de browser. Server Components îți dau acces direct la baza de date, bundle mai mic și secrete protejate. Nu e „server e mai bun decât client" — sunt două unelte: server pentru date și conținut, client pentru interactivitate.
modul 03 / Server Components vs Client Components
compunerea corectă: unde pui granița
Aplicațiile reale amestecă ambele tipuri. Arta e să pui granița 'use client' cât mai jos în arbore — adică pe cea mai mică componentă care chiar are nevoie.
„use client" se propagă în jos
Când marchezi o componentă cu 'use client', tot ce importă ea devine parte din bundle-ul clientului. De aceea, dacă pui directiva pe un layout mare, trimiți în browser mult mai mult cod decât e nevoie.
❌ Greșit: 'use client' pe toată pagina, doar pentru un buton
→ tot arborele ajunge în browser
✅ Corect: pagina rămâne Server Component,
iar butonul interactiv e o componentă separată cu 'use client'
tiparul: server aduce datele, client le face interactive
// app/produse/page.tsx — Server Component
import AdaugaInCos from './AdaugaInCos'; // Client Component
export default async function Pagina() {
const produse = await db.produse.findMany(); // pe server
return produse.map(p => (
<div key={p.id}>
<h3>{p.nume}</h3>
<AdaugaInCos id={p.id} /> {/* doar butonul e client */}
</div>
));
}
regula care surprinde: children ca „gaură"
Un Client Component nu poate importa un Server Component. DAR poate primi unul ca prop (de obicei children) — pentru că acela e randat pe server și trimis deja ca rezultat:
// Server Component (părinte)
<Accordeon> {/* Client: gestionează deschis/închis */}
<ContinutDeServer /> {/* Server: rămâne pe server */}
</Accordeon>
Acest tipar îți permite să ai un înveliș interactiv (client) cu conținut greu adus de pe server, fără să tragi conținutul în bundle.
ce trece prin graniță: doar date serializabile
Props-urile trimise de la Server la Client trebuie să fie serializabile (obiecte, string-uri, numere, array-uri). Nu poți trimite funcții ca props unui Client Component (excepție: Server Actions — modulul 7). E o consecință logică: props-urile trebuie „împachetate" și trimise prin rețea.
de reținut
Pune 'use client' cât mai jos în arbore, doar pe componentele care chiar au nevoie — directiva se propagă la tot ce importă acea componentă. Tiparul sănătos: serverul aduce datele, clientul adaugă interactivitatea. Un Client Component nu poate importa un Server Component, dar îl poate primi ca children. Iar prin graniță trec doar date serializabile, nu funcții.
modul 04 / stilizare, imagini & fonturi
stilizare: CSS Modules & Tailwind
Next.js suportă mai multe abordări de stilizare. Nu există „una corectă" — dar merită să înțelegi ce rezolvă fiecare.
CSS global
// app/layout.tsx
import './globals.css'; // se importă o singură dată, în layout-ul rădăcină
Pentru reset-uri, variabile CSS, stiluri de bază. Se importă doar în layout-ul rădăcină.
CSS Modules — stiluri izolate
Un fișier .module.css generează nume de clase unice, deci stilurile nu se pot ciocni între componente:
/* Card.module.css */
.card { padding: 1rem; border-radius: 12px; }
import stiluri from './Card.module.css';
export default function Card() {
return <div className={stiluri.card}>…</div>;
}
Clasa .card devine ceva de tipul Card_card__x7f2 la build — imposibil de suprascris accidental din altă parte. E soluția clasică pentru izolare, fără dependințe externe.
Tailwind CSS
Opțiunea propusă implicit de create-next-app: scrii clase utilitare direct în markup.
<div className="p-4 rounded-xl bg-neutral-900 text-white">…</div>
- Rapid pentru prototipare și consistent (valori dintr-o scară predefinită).
- Nu comuți între fișiere; stilul stă lângă markup.
- Compromis: markup mai încărcat vizual.
| Abordare | Bună când |
| CSS global | Reset, variabile, stiluri de bază |
| CSS Modules | Vrei izolare fără dependințe, CSS clasic |
| Tailwind | Viteză, consistență, echipe care preferă utilitare |
de reținut
Trei niveluri: CSS global (o dată, în layout-ul rădăcină, pentru bazele), CSS Modules (clase izolate automat, fără ciocniri) și Tailwind (utilitare în markup, rapid și consistent). Alegerea e de echipă și de gust — important e să nu amesteci haotic. Ce contează tehnic: CSS Modules garantează izolarea, iar Next.js încarcă doar CSS-ul folosit de rutele vizitate.
modul 04 / stilizare, imagini & fonturi
next/image & next/font
Imaginile și fonturile sunt cele mai frecvente cauze ale unui site lent. Next.js are componente dedicate care rezolvă asta automat — două dintre cele mai concrete beneficii ale framework-ului.
componenta Image
import Image from 'next/image';
<Image
src="/hero.jpg"
width={1200}
height={630}
alt="Descriere pentru accesibilitate"
priority {/* pentru imaginea principală, vizibilă imediat */}
/>
ce face automat
- Formate moderne — servește WebP/AVIF browserelor care le suportă, fără să convertești tu nimic.
- Dimensiuni potrivite — generează variante pentru diferite ecrane; telefonul nu descarcă imaginea de desktop.
- Lazy loading implicit — imaginile de sub „fold" se încarcă abia când sunt necesare.
- Previne layout shift — cu width/height rezervă spațiul, deci pagina nu „sare" când se încarcă imaginea (important pentru scorul Core Web Vitals).
width/height nu sunt „mărimea afișată"
Ele descriu raportul și dimensiunea intrinsecă, ca Next.js să rezerve spațiul corect. Mărimea vizuală o controlezi din CSS. Folosește priority DOAR pentru imaginea principală vizibilă imediat (hero) — pusă peste tot, anulează beneficiul lazy loading-ului.
next/font — fonturi fără penalizare
import { Space_Grotesk } from 'next/font/google';
const display = Space_Grotesk({
subsets: ['latin', 'latin-ext'], // latin-ext = diacritice românești
display: 'swap',
});
export default function RootLayout({ children }) {
return <html lang="ro" className={display.className}>…</html>;
}
- Self-hosting automat — fontul e descărcat la build și servit de pe domeniul tău: zero cereri către Google, mai rapid și mai bun pentru confidențialitate.
- Fără layout shift — Next.js calculează metrici de rezervă, deci textul nu „sare" când se încarcă fontul.
- Subsets — pentru română include latin-ext, altfel diacriticele (ă, â, î, ș, ț) pot cădea pe un font de rezervă.
de reținut
next/image îți dă gratuit formate moderne, dimensiuni per ecran, lazy loading și zero layout shift — folosește priority doar pentru hero. next/font face self-hosting automat al fonturilor, elimină layout shift-ul și îți cere să incluzi latin-ext pentru diacriticele românești. Sunt două optimizări pe care în React „gol" le-ai face manual, prost și târziu.
modul 05 / aducerea datelor & caching
fetch în Server Components
În App Router, aducerea datelor e surprinzător de simplă: scrii await direct în componentă. Fără useEffect, fără stări de încărcare scrise manual, fără biblioteci de data fetching pentru cazurile obișnuite.
tiparul de bază
// app/produse/page.tsx — Server Component
export default async function Produse() {
const res = await fetch('https://api.exemplu.ro/produse');
const produse = await res.json();
return <ul>{produse.map(p => <li key={p.id}>{p.nume}</li>)}</ul>;
}
Compară cu React clasic: acolo ai nevoie de useEffect + useState + gestionarea manuală a stărilor de încărcare și eroare. Aici, componenta e async și aștepți direct.
de ce e mai bine
- Fără waterfall client-server — datele se aduc pe server, aproape de sursă, nu după ce browserul a descărcat JS-ul.
- Fără API intermediar — poți interoga direct baza de date; nu construiești un endpoint doar ca să-ți hrănești propriul front-end.
- loading.tsx îți dă starea de încărcare automat (modulul 2).
- Secretele rămân pe server — cheile API nu ajung în browser.
cereri paralele — evită waterfall-ul
Dacă faci două await unul după altul, a doua cerere așteaptă degeaba după prima:
❌ secvențial (lent):
const user = await getUser();
const posts = await getPosts(); // a așteptat degeaba
✅ paralel (rapid):
const [user, posts] = await Promise.all([
getUser(),
getPosts(),
]);
Folosește Promise.all când cererile nu depind una de alta. E una dintre cele mai frecvente optimizări ratate.
deduplicarea automată
Dacă două componente diferite cer aceeași resursă în timpul aceleiași randări, Next.js face o singură cerere și o refolosește. Deci nu trebuie să „ridici" datele într-un părinte doar ca să eviți duplicarea — poți cere datele fix acolo unde ai nevoie de ele.
de reținut
În Server Components aduci datele cu await direct în componentă — fără useEffect, fără API intermediar, cu secretele protejate. Două lucruri de reținut pentru performanță: folosește Promise.all pentru cereri independente (altfel creezi un waterfall inutil), și bazează-te pe deduplicarea automată — cere datele acolo unde ai nevoie de ele, fără să le ridici artificial în părinte.
modul 05 / aducerea datelor & caching
caching, revalidare & randare dinamică
Caching-ul e partea care încurcă cel mai des. Ideea de bază: Next.js vrea să facă pagina cât mai statică posibil, iar tu îi spui explicit când datele trebuie să fie proaspete.
controlul cache-ului per cerere
// 1. cache (implicit în multe cazuri): rezultat refolosit
await fetch(url, { cache: 'force-cache' });
// 2. mereu proaspăt: se cere la fiecare afișare
await fetch(url, { cache: 'no-store' });
// 3. revalidare în timp (ISR): reîmprospătat la 60s
await fetch(url, { next: { revalidate: 60 } });
| Strategie | Folosești pentru |
| cache | Date care se schimbă rar (pagini de conținut, liste stabile) |
| no-store | Date personalizate sau critice (coș, dashboard, stocuri live) |
| revalidate: N | Compromisul ideal: rapid ca static, dar reîmprospătat periodic |
atenție la versiuni
Comportamentul implicit de cache s-a schimbat între versiunile majore de Next.js. Nu te baza pe memorie sau pe tutoriale vechi: fii explicit (scrie ce vrei: no-store / revalidate) și verifică documentația versiunii pe care o folosești. Explicit e mereu mai sigur decât implicit.
revalidare la cerere (on-demand)
Uneori nu vrei un interval de timp, ci „reîmprospătează ACUM, pentru că s-a schimbat ceva" (ex: după publicarea unui articol):
import { revalidatePath, revalidateTag } from 'next/cache';
revalidatePath('/blog'); // invalidează o rută
revalidateTag('articole'); // invalidează tot ce e etichetat
Le folosești tipic după o mutație (modulul 7), ca utilizatorul să vadă imediat datele actualizate.
static vs dinamic — ce declanșează ce
O rută devine dinamică (randată la fiecare cerere) dacă folosește lucruri care depind de cerere:
- cookies(), headers() — depind de cine cere pagina.
- searchParams — depinde de URL-ul cerut.
- Un fetch cu no-store.
Altfel, Next.js o va face statică la build (cel mai rapid). Cu generateStaticParams poți pre-genera și paginile dinamice (ex: toate articolele de blog) ca fișiere statice.
de reținut
Next.js preferă static și devine dinamic doar când e nevoie (cookies, headers, searchParams, no-store). Tu controlezi prospețimea explicit: no-store (mereu proaspăt), revalidate: N (ISR — rapid dar actualizat), plus revalidatePath/revalidateTag pentru invalidare la cerere după modificări. Regula practică: fii explicit, pentru că implicitul diferă între versiuni.
modul 06 / streaming & Suspense
problema: o cerere lentă blochează toată pagina
Imaginează un dashboard: antetul și navigația sunt instant, dar un grafic are nevoie de o interogare lentă (2 secunde). Fără streaming, utilizatorul așteaptă 2 secunde pe alb — deși 90% din pagină era gata imediat.
ce e streaming-ul
Streaming înseamnă că serverul trimite HTML-ul pe bucăți, pe măsură ce e gata, în loc să aștepte întreaga pagină. Părțile rapide apar imediat; cele lente sosesc când sunt gata și se completează la locul lor.
Fără streaming: [aștept tot] ──────────► pagina completă (2s)
Cu streaming: [antet+nav] ──► (instant)
[grafic] ──► (la 2s, în locul lui)
două niveluri de streaming
- Pe pagină — fișierul loading.tsx (modulul 2): întreaga pagină are o stare de încărcare, iar layout-ul rămâne vizibil.
- Pe componentă — <Suspense>: controlezi exact ce bucată se încarcă separat. Mult mai fin.
Suspense în practică
import { Suspense } from 'react';
export default function Dashboard() {
return (
<>
<Antet /> {/* instant */}
<Suspense fallback={<SkeletonGrafic />}>
<GraficLent /> {/* sosește când e gata */}
</Suspense>
<Suspense fallback={<SkeletonTabel />}>
<TabelLent />
</Suspense>
</>
);
}
Fiecare bucată învelită în Suspense se încarcă independent: graficul poate sosi înaintea tabelului, fără să se blocheze reciproc. fallback e ce se afișează între timp.
de reținut
Streaming = serverul trimite HTML pe bucăți, pe măsură ce sunt gata, în loc să blocheze pagina după cea mai lentă cerere. Ai două unelte: loading.tsx (la nivel de pagină, automat) și <Suspense> (control fin, per componentă, cu fallback propriu). Regula: izolează în Suspense partea lentă, ca restul paginii să apară imediat.
modul 06 / streaming & Suspense
skeleton-uri & unde pui granițele
fallback bun = skeleton, nu spinner
Un skeleton (o schiță gri a conținutului care va veni) e superior unui spinner: utilizatorul vede forma paginii, deci percepe încărcarea ca fiind mai rapidă și nu se produce salt de layout când sosește conținutul real.
function SkeletonCard() {
return (
<div className="animate-pulse">
<div className="h-6 w-2/3 bg-neutral-800 rounded" />
<div className="h-4 w-full bg-neutral-800 rounded mt-3" />
</div>
);
}
Ideal, skeleton-ul are aceleași dimensiuni ca finalul — astfel pagina nu „sare" la înlocuire.
unde pui granița de Suspense
| Situație | Abordare |
| Toată pagina depinde de o singură cerere | loading.tsx e suficient |
| O bucată e lentă, restul rapid | Suspense doar pe bucata lentă |
| Mai multe zone lente independente | Câte un Suspense pe fiecare — sosesc separat |
| Componente mici, rapide | Fără Suspense (adaugi complexitate degeaba) |
greșeli frecvente
- Suspense prea sus — dacă înveți toată pagina, ai pierdut avantajul: iar aștepți totul.
- Fetch în părinte, nu în copil — dacă aduci datele în componenta părinte și le pasezi în jos, părintele se blochează oricum. Ca streaming-ul să funcționeze, componenta învelită în Suspense trebuie să-și aducă singură datele.
- Prea multe granițe — pagina apare pe fragmente și pare agitată; grupează logic.
de ce contează pentru utilizator
Streaming-ul îmbunătățește viteza percepută și metricile Core Web Vitals: conținutul apare devreme, nu după cea mai lentă cerere. E diferența dintre „site-ul e lent" și „site-ul se încarcă progresiv" — chiar dacă timpul total e același.
de reținut
Folosește skeleton-uri de aceeași formă cu conținutul final, nu spinnere. Pune granița de Suspense fix pe bucata lentă, iar acea componentă trebuie să-și aducă singură datele (altfel părintele se blochează oricum și streaming-ul n-are efect). Nici prea sus (blochezi tot), nici prea multe granițe (pagina pare agitată). Câștigul e viteza percepută — utilizatorul vede conținut imediat.
modul 07 / mutații: Server Actions & formulare
Server Actions: funcții care rulează pe server
Până acum am citit date. Dar aplicațiile trebuie și să le modifice: creare, editare, ștergere. În mod clasic ai scrie un endpoint API, un fetch din client, gestionarea stărilor. Server Actions elimină aproape tot acest cod.
ce e o Server Action
O funcție marcată cu 'use server' care rulează pe server, dar pe care o poți apela direct din componente — inclusiv dintr-un formular. Next.js creează automat legătura prin rețea.
// app/actions.ts
'use server';
export async function creeazaArticol(formData) {
const titlu = formData.get('titlu');
await db.articole.create({ data: { titlu } }); // direct în DB
}
folosită într-un formular
import { creeazaArticol } from './actions';
export default function Formular() {
return (
<form action={creeazaArticol}>
<input name="titlu" required />
<button type="submit">Publică</button>
</form>
);
}
Observă: fără endpoint API scris de tine, fără fetch, fără onSubmit, fără useState pentru câmpuri. Formularul trimite direct către funcția de server.
de ce e important: funcționează și fără JavaScript
Pentru că folosește un <form> real cu action, trimiterea funcționează chiar dacă JavaScript-ul nu s-a încărcat încă sau e blocat. Apoi, când JS e disponibil, Next.js îmbunătățește experiența (fără reîncărcare). Asta e progressive enhancement — accesibilitate și robustețe reale, nu doar teorie.
revalidarea după mutație
După ce ai modificat date, cache-ul trebuie actualizat, altfel utilizatorul vede informația veche:
'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
export async function creeazaArticol(formData) {
await db.articole.create({ data: { titlu: formData.get('titlu') } });
revalidatePath('/blog'); // lista se reîmprospătează
redirect('/blog'); // duci utilizatorul la listă
}
de reținut
Server Actions („use server") sunt funcții care rulează pe server dar se apelează direct din componente/formulare — elimină endpoint-ul API, fetch-ul și gestionarea manuală a stării. Marele avantaj practic: fiind un <form> real, funcționează și fără JavaScript (progressive enhancement). După orice mutație, cheamă revalidatePath/revalidateTag, altfel utilizatorul rămâne cu date vechi.
modul 07 / mutații: Server Actions & formulare
validare, stări & feedback în formulare
Un formular real are nevoie de validare, mesaje de eroare și feedback în timpul trimiterii. Next.js și React oferă hooks dedicate.
validarea pe server — obligatorie
regula de securitate
Validarea din browser e doar pentru experiența utilizatorului — poate fi ocolită trivial. Validarea pe server e cea care contează pentru securitate și integritatea datelor. Validează MEREU în Server Action, indiferent ce verificări ai în UI. Aceeași regulă din orice back-end (ai văzut-o și la Laravel).
'use server';
export async function creeaza(prevState, formData) {
const titlu = String(formData.get('titlu') ?? '').trim();
if (titlu.length < 3) {
return { eroare: 'Titlul trebuie să aibă minim 3 caractere.' };
}
await db.articole.create({ data: { titlu } });
revalidatePath('/blog');
return { succes: true };
}
useActionState — starea răspunsului
Ca să afișezi mesajul întors de acțiune, folosești hook-ul useActionState (într-un Client Component):
'use client';
import { useActionState } from 'react';
import { creeaza } from './actions';
export default function Formular() {
const [stare, actiune] = useActionState(creeaza, {});
return (
<form action={actiune}>
<input name="titlu" />
{stare.eroare && <p className="eroare">{stare.eroare}</p>}
<Trimite />
</form>
);
}
useFormStatus — feedback în timpul trimiterii
'use client';
import { useFormStatus } from 'react-dom';
function Trimite() {
const { pending } = useFormStatus();
return (
<button disabled={pending}>
{pending ? 'Se trimite…' : 'Publică'}
</button>
);
}
useFormStatus trebuie apelat într-o componentă copil a formularului (ca aici, butonul) — nu în componenta care conține <form>.
securitatea Server Actions
- O Server Action e, practic, un endpoint public — oricine îi poate trimite date.
- Deci: verifică autentificarea și permisiunile în interiorul acțiunii, nu doar ascunzi butonul în UI.
- Validează și sanitizează tot ce vine din formular.
de reținut
Validează mereu pe server (validarea din browser e doar pentru UX și poate fi ocolită). Pentru feedback: useActionState îți dă răspunsul acțiunii (erori/succes), iar useFormStatus starea de trimitere (pending), apelat într-o componentă copil a formularului. Și reține regula de securitate: o Server Action e un endpoint public — verifică autentificarea și permisiunile înăuntrul ei.
modul 08 / Route Handlers & middleware
Route Handlers: API în App Router
Am văzut că, în Server Components, nu ai nevoie de API ca să-ți aduci propriile date. Dar uneori chiar ai nevoie de un endpoint public: pentru un webhook, o aplicație mobilă, un client extern sau o integrare.
route.ts în loc de page.tsx
Într-un folder pui route.ts (nu page.tsx) și exporți funcții numite după metodele HTTP:
// app/api/produse/route.ts → /api/produse
export async function GET() {
const produse = await db.produse.findMany();
return Response.json(produse);
}
export async function POST(request) {
const date = await request.json();
const nou = await db.produse.create({ data: date });
return Response.json(nou, { status: 201 });
}
regulă
Un folder poate avea fie page.tsx, fie route.ts — nu ambele. Unul servește HTML, celălalt servește date.
parametri & query
// app/api/produse/[id]/route.ts
export async function GET(request, { params }) {
const { id } = await params; // din URL
const { searchParams } = new URL(request.url);
const camp = searchParams.get('camp'); // din ?camp=…
const produs = await db.produse.findUnique({ where: { id } });
if (!produs) return new Response('Negăsit', { status: 404 });
return Response.json(produs);
}
când ai nevoie (și când nu)
| Ai nevoie de Route Handler | NU ai nevoie |
| Webhook-uri de la servicii externe (plăți, CMS) | Ca să aduci date pentru propriile pagini |
| API pentru o aplicație mobilă / client extern | Ca să faci o mutație dintr-un formular |
| Endpoint-uri care întorc fișiere, feed-uri RSS, imagini OG | (acolo folosești Server Components / Server Actions) |
Greșeala frecventă a celor veniți din Pages Router: creează endpoint-uri API pentru fiecare pagină și apoi le consumă cu fetch din client. În App Router, pentru datele proprii mergi direct — e mai rapid și mai simplu.
de reținut
route.ts creează endpoint-uri (exporți GET/POST/PUT/DELETE) — un folder are fie page.tsx, fie route.ts. Le folosești pentru consumatori externi: webhook-uri, aplicații mobile, feed-uri. Pentru propriile pagini nu-ți trebuie API: citești cu Server Components, modifici cu Server Actions. Reține distincția, ca să nu construiești un strat inutil.
modul 08 / Route Handlers & middleware
middleware: cod înainte de fiecare cerere
Middleware e cod care rulează înainte ca cererea să ajungă la o rută. E locul potrivit pentru decizii globale: redirecționări, protejarea rutelor, localizare, antete.
fișierul middleware.ts
// middleware.ts (în rădăcina proiectului)
import { NextResponse } from 'next/server';
export function middleware(request) {
const token = request.cookies.get('sesiune');
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next(); // lasă cererea să continue
}
export const config = {
matcher: ['/dashboard/:path*'], // pe ce rute rulează
};
matcher — limitează unde rulează
Fără matcher, middleware-ul rulează la fiecare cerere, inclusiv pentru fișiere statice — risipă. Cu matcher îl restrângi exact la rutele care contează.
ce poate face
| Acțiune | Exemplu |
| redirect | Neautentificat → /login |
| rewrite | Servește alt conținut păstrând URL-ul (A/B testing, multi-tenant) |
| antete/cookies | Setează antete de securitate, citește preferințe |
| localizare | Detectează limba și redirecționează spre /ro sau /en |
limitări importante
- Rulează într-un mediu restrâns (Edge) — nu ai acces la tot ce ai în Node.js; multe biblioteci grele nu funcționează.
- Trebuie să fie foarte rapid — rulează înaintea fiecărei cereri care se potrivește; orice lentoare afectează tot site-ul.
- Nu face interogări grele în DB aici. Verifică doar existența/validitatea unui cookie de sesiune și lasă verificările detaliate în paginile/acțiunile propriu-zise.
de reținut
middleware.ts rulează înaintea cererii: perfect pentru redirecționări, rescrieri, antete și localizare. Restrânge-l cu matcher ca să nu ruleze inutil pe toate resursele. Limitările contează: mediu Edge restrâns și cerință de viteză — deci fă aici doar verificări rapide (există cookie-ul de sesiune?), nu logică grea sau interogări de bază de date.
modul 09 / autentificare & protejarea rutelor
sesiuni & fluxul de autentificare
Autentificarea în Next.js are o particularitate: aplicația rulează și pe server, și în client, deci trebuie să știi unde verifici identitatea. Vestea bună: Server Components fac lucrurile mai sigure implicit.
fluxul tipic
- Utilizatorul trimite email + parolă (formular cu Server Action).
- Serverul verifică datele față de baza de date (parola stocată hash-uită, niciodată în clar).
- Se creează o sesiune și se pune într-un cookie httpOnly.
- La cererile următoare, serverul citește cookie-ul și știe cine e utilizatorul.
cookie httpOnly — de ce contează
import { cookies } from 'next/headers';
const c = await cookies();
c.set('sesiune', token, {
httpOnly: true, // JS din browser NU îl poate citi (anti-XSS)
secure: true, // doar pe HTTPS
sameSite: 'lax', // protecție CSRF
path: '/',
});
Un token pus în localStorage poate fi furat de orice script injectat (XSS). Un cookie httpOnly nu e accesibil din JavaScript — de aceea e standardul pentru sesiuni.
citirea utilizatorului curent
// funcție refolosibilă, pe server
import { cookies } from 'next/headers';
export async function getUtilizator() {
const token = (await cookies()).get('sesiune')?.value;
if (!token) return null;
return await verificaSesiune(token); // întoarce user sau null
}
O folosești în orice Server Component, layout sau Server Action. Pentru că rulează pe server, verificarea e reală — nu poate fi ocolită din browser.
biblioteci
Poți implementa autentificarea manual (ca mai sus) sau folosi soluții consacrate (Auth.js/NextAuth, Clerk, Supabase Auth) care aduc login social, resetare de parolă și gestionarea sesiunilor. Principiile rămân aceleași — important e să înțelegi ce fac dedesubt.
de reținut
Fluxul: formular (Server Action) → verificare pe server → sesiune într-un cookie httpOnly + secure + sameSite. Nu ține token-uri în localStorage (vulnerabil la XSS). Construiește o funcție getUtilizator() pe server, refolosibilă în componente, layout-uri și acțiuni. Bibliotecile îți economisesc timp, dar principiile de securitate rămân responsabilitatea ta.
modul 09 / autentificare & protejarea rutelor
protejarea rutelor: unde verifici cu adevărat
Aici se fac cele mai multe greșeli de securitate în aplicațiile Next.js. Regula fundamentală: protecția reală se face acolo unde sunt datele, nu în UI.
trei straturi, cu roluri diferite
| Strat | Rol | E suficient? |
| Middleware | Redirecționare rapidă (UX): fără cookie → /login | ❌ Nu — doar comoditate |
| Pagină / layout (server) | Verifică sesiunea înainte de a randa conținut | ✅ Pentru afișare |
| Server Action / Route Handler | Verifică ÎNAINTE de orice operație pe date | ✅ Obligatoriu |
protejarea unei pagini
import { redirect } from 'next/navigation';
import { getUtilizator } from '@/lib/auth';
export default async function Dashboard() {
const user = await getUtilizator();
if (!user) redirect('/login');
return <h1>Salut, {user.nume}</h1>;
}
protejarea unei acțiuni — cea mai importantă
'use server';
export async function stergeArticol(id) {
const user = await getUtilizator();
if (!user) throw new Error('Neautentificat');
const articol = await db.articole.findUnique({ where: { id } });
// autorizare: e chiar articolul lui?
if (articol.autorId !== user.id) throw new Error('Interzis');
await db.articole.delete({ where: { id } });
revalidatePath('/articole');
}
autentificare ≠ autorizare
Autentificare = cine ești (ești logat?). Autorizare = ai voie să faci ASTA? Un utilizator logat nu are automat dreptul să șteargă articolul altcuiva. Verifică ambele — exact ca la Laravel (gates/policies).
greșeli frecvente
- „Ascund butonul în UI, deci e protejat" — fals. Oricine poate apela acțiunea direct. UI-ul nu e o barieră de securitate.
- Verific doar în middleware — insuficient: middleware-ul e pentru redirecționare rapidă, nu poate face verificări profunde.
- Date sensibile trimise ca props către Client Components — orice trimiți spre client e vizibil în browser. Filtrează pe server ce trimiți (nu pasa obiectul complet cu hash de parolă, email intern etc.).
de reținut
Protecția reală stă lângă date: în Server Actions și Route Handlers, verifici mereu autentificarea ȘI autorizarea înainte de operație. Middleware-ul e doar UX (redirecționare rapidă), iar ascunderea butoanelor nu protejează nimic. Atenție și la ce props trimiți spre Client Components — tot ce trece granița e vizibil în browser, deci filtrează pe server.
modul 10 / optimizare, SEO, deploy · proiect & test final
performanță, SEO & deploy
optimizări de performanță
SEO tehnic
| Element | Cum îl faci |
| title / description | metadata sau generateMetadata (modulul 2) |
| canonical & Open Graph | metadata.alternates.canonical, metadata.openGraph |
| sitemap.xml | app/sitemap.ts — îl generezi din date |
| robots.txt | app/robots.ts |
| date structurate | JSON-LD într-un <script type="application/ld+json"> |
// app/sitemap.ts
export default async function sitemap() {
const articole = await getArticole();
return [
{ url: 'https://site.ro', lastModified: new Date() },
...articole.map(a => ({
url: `https://site.ro/blog/${'{'}a.slug{'}'}`,
lastModified: a.actualizatLa,
})),
];
}
variabile de mediu
# .env.local (NU se comite în Git)
DATABASE_URL=postgres://... # doar pe server
NEXT_PUBLIC_SITE_URL=https://site.ro # ajunge în browser
regulă critică
Doar variabilele care încep cu NEXT_PUBLIC_ ajung în browser — oricine le poate vedea. Cheile secrete (baze de date, API keys private) NU trebuie să aibă acest prefix. O cheie secretă cu prefix NEXT_PUBLIC_ e o scurgere de securitate publicată la fiecare build.
deploy
- Vercel — platforma creatorilor Next.js: conectezi repo-ul Git, fiecare push face deploy, iar funcțiile de server/ISR merg fără configurare.
- Alte platforme (Netlify, Railway, VPS cu Node) — funcționează, dar unele funcții avansate cer configurări suplimentare.
- Export static — dacă aplicația nu are nevoie de server (doar pagini statice), poți exporta HTML static și îl găzduiești oriunde (inclusiv Cloudflare Pages). Renunți însă la SSR, ISR și Server Actions.
de reținut
Producția înseamnă: bundle mic (use client jos, dynamic import), imagini/fonturi optimizate, static unde se poate, streaming pe zonele lente. Pentru SEO ai metadata, sitemap.ts, robots.ts, JSON-LD. Iar regula de securitate de reținut: NEXT_PUBLIC_ = public — niciodată pentru secrete. Deploy: Vercel e calea fără fricțiune; export static doar dacă renunți la funcțiile de server.
modul 10 / optimizare, SEO, deploy · proiect & test final
proiect ghidat: blog cu panou de administrare
Pui tot cursul la lucru construind o aplicație completă, care folosește fiecare concept învățat. E proiectul standard prin care se demonstrează stăpânirea App Router.
ce construiești
Un blog public + panou de administrare protejat: vizitatorii citesc articolele, iar autorul autentificat le creează, editează și șterge.
structura rutelor
app/
layout.tsx # header, footer, fonturi
page.tsx # pagina principală
blog/
page.tsx # lista (statică + revalidate)
loading.tsx
[slug]/page.tsx # articol + generateMetadata
(admin)/
layout.tsx # verifică sesiunea
dashboard/page.tsx
dashboard/nou/page.tsx # formular cu Server Action
login/page.tsx
actions.ts # Server Actions
sitemap.ts
planul, pe modulele cursului
- Setup (mod. 0–1): create-next-app, rutele publice, navigare cu Link.
- Layout & fișiere speciale (mod. 2): layout rădăcină, loading.tsx, error.tsx, not-found.tsx.
- Server/Client (mod. 3): paginile sunt Server Components; doar butoanele interactive au „use client".
- Stil & media (mod. 4): Tailwind sau CSS Modules, next/font cu latin-ext, next/image pentru copertele articolelor.
- Date (mod. 5): lista de articole statică cu revalidate; articolul individual pre-generat cu generateStaticParams.
- Streaming (mod. 6): secțiunea de comentarii (lentă) izolată în <Suspense> cu skeleton.
- Mutații (mod. 7): creare/editare/ștergere prin Server Actions, validare pe server, useActionState pentru erori, revalidatePath după fiecare mutație.
- API & middleware (mod. 8): un route.ts pentru feed RSS; middleware care redirecționează /dashboard fără sesiune.
- Auth (mod. 9): login cu Server Action, cookie httpOnly, verificare în layout-ul (admin) ȘI în fiecare acțiune (autorizare: e articolul lui?).
- Producție (mod. 10): metadata + generateMetadata, sitemap.ts, robots.ts, variabile de mediu, deploy.
definiția lui „gata"
- Blogul public e rapid (static/ISR), cu metadata corectă per articol și imagini optimizate.
- Panoul de administrare e protejat la nivel de acțiune, nu doar ascuns în UI.
- Formularele funcționează și fără JavaScript, afișează erori de validare de la server.
- După fiecare mutație, listele se actualizează (revalidate) — fără date vechi.
- Zona lentă e izolată în Suspense; pagina apare imediat.
- Niciun secret nu are prefixul NEXT_PUBLIC_; aplicația e live.
felicitări
Ai parcurs Next.js modern cap-coadă: rutare bazată pe fișiere, layouts imbricate, Server vs Client Components, date & caching, streaming cu Suspense, Server Actions, Route Handlers & middleware, autentificare, SEO și deploy — combinate într-un blog cu panou de administrare. Ai acum modelul mental corect: server implicit, client doar unde e nevoie. Urmează testul final: 20 de întrebări din tot cursul. Prag: 70%. Succes!