2.8 — SvelteKit vs Next.js

Comparando os meta-frameworks fullstack.

Objetivos da Aula

  • Comparar arquiteturas do SvelteKit e Next.js
  • Entender diferenças em roteamento, data loading e APIs
  • Ver equivalências de features

O Que São Meta-Frameworks?

FRAMEWORK vs META-FRAMEWORK
Framework (Svelte, React):
  • → Biblioteca para criar componentes UI
  • → Não opinado sobre roteamento, fetching, etc
Meta-Framework (SvelteKit, Next.js):
  • → Framework completo para apps fullstack
  • → Roteamento, SSR, API routes, deploy
  • → Opinado (convenções sobre configuração)
Svelte→ SvelteKit React→ Next.js, Remix Vue→ Nuxt

Roteamento

Next.js (App Router)

app/
page.tsx→ / about/page.tsx→ /about blog/page.tsx→ /blog blog/[slug]/page.tsx→ /blog/:slug api/users/route.ts→ /api/users layout.tsx→ Layout raiz

SvelteKit

src/routes/
+page.svelte→ / about/+page.svelte→ /about blog/+page.svelte→ /blog blog/[slug]/+page.svelte→ /blog/:slug api/users/+server.js→ /api/users +layout.svelte→ Layout raiz

Diferenças Principais

AspectoNext.jsSvelteKit
Arquivo de páginapage.tsx+page.svelte
Layoutlayout.tsx+layout.svelte
API routeroute.ts+server.js
Loading stateloading.tsxManual ou streaming
Errorerror.tsx+error.svelte
PrefixoNenhum+ (distingue de componentes)

Data Loading

Next.js (Server Components)

// app/users/page.tsx

// Componente é async por padrão (Server Component)
export default async function UsersPage() {
  // Fetch acontece no servidor
  const users = await fetch('https://api.example.com/users')
    .then(r => r.json())

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

// Metadata
export const metadata = {
  title: 'Usuários'
}

SvelteKit

<!-- src/routes/users/+page.svelte -->
<script>
  export let data  // Vem da função load
</script>

<ul>
  {#each data.users as user (user.id)}
    <li>{user.name}</li>
  {/each}
</ul>
// src/routes/users/+page.server.js
export async function load({ fetch }) {
  const users = await fetch('https://api.example.com/users')
    .then(r => r.json())

  return { users }
}
// src/routes/users/+page.js (metadata)
export const load = async () => {
  return {
    title: 'Usuários'  // Disponível em +layout.svelte
  }
}

Diferenças

AspectoNext.jsSvelteKit
ModeloServer Componentsload functions
Onde definirNo próprio componenteArquivo separado (+page.server.js)
Dados no cliente'use client' + hooks+page.js (universal)
InvalidaçãorevalidatePath()invalidate()

API Routes

Next.js

// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function GET(request: NextRequest) {
  const users = await db.users.findMany()
  return NextResponse.json(users)
}

export async function POST(request: NextRequest) {
  const body = await request.json()
  const user = await db.users.create({ data: body })
  return NextResponse.json(user, { status: 201 })
}

// Parâmetros dinâmicos: app/api/users/[id]/route.ts
export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const user = await db.users.findUnique({
    where: { id: params.id }
  })
  return NextResponse.json(user)
}

SvelteKit

// src/routes/api/users/+server.js
import { json } from '@sveltejs/kit'

export async function GET({ url }) {
  const users = await db.users.findMany()
  return json(users)
}

export async function POST({ request }) {
  const body = await request.json()
  const user = await db.users.create({ data: body })
  return json(user, { status: 201 })
}

// Parâmetros dinâmicos: src/routes/api/users/[id]/+server.js
export async function GET({ params }) {
  const user = await db.users.findUnique({
    where: { id: params.id }
  })
  return json(user)
}

Form Handling

Next.js (Server Actions)

// app/contact/page.tsx
async function submitForm(formData: FormData) {
  'use server'

  const name = formData.get('name')
  const email = formData.get('email')

  await db.contacts.create({ data: { name, email } })

  revalidatePath('/contacts')
}

export default function ContactPage() {
  return (
    <form action={submitForm}>
      <input name="name" required />
      <input name="email" type="email" required />
      <button type="submit">Enviar</button>
    </form>
  )
}

SvelteKit (Form Actions)

<!-- src/routes/contact/+page.svelte -->
<script>
  import { enhance } from '$app/forms'
</script>

<form method="POST" use:enhance>
  <input name="name" required />
  <input name="email" type="email" required />
  <button type="submit">Enviar</button>
</form>
// src/routes/contact/+page.server.js
import { fail, redirect } from '@sveltejs/kit'

export const actions = {
  default: async ({ request }) => {
    const data = await request.formData()
    const name = data.get('name')
    const email = data.get('email')

    // Validação
    if (!name) {
      return fail(400, { name, missing: true })
    }

    await db.contacts.create({ data: { name, email } })

    throw redirect(303, '/contacts')
  }
}

Layouts

Next.js

// app/layout.tsx (root)
export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <Header />
        {children}
        <Footer />
      </body>
    </html>
  )
}

// app/dashboard/layout.tsx (nested)
export default function DashboardLayout({ children }) {
  return (
    <div className="dashboard">
      <Sidebar />
      <main>{children}</main>
    </div>
  )
}

SvelteKit

<!-- src/routes/+layout.svelte (root) -->
<script>
  import Header from '$lib/Header.svelte'
  import Footer from '$lib/Footer.svelte'
</script>

<Header />
<slot />
<Footer />

<!-- src/routes/dashboard/+layout.svelte (nested) -->
<script>
  import Sidebar from '$lib/Sidebar.svelte'
</script>

<div class="dashboard">
  <Sidebar />
  <main>
    <slot />
  </main>
</div>

Middleware / Hooks

Next.js (middleware.ts)

// middleware.ts (na raiz)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // Verificar autenticação
  const token = request.cookies.get('token')

  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: '/dashboard/:path*'
}

SvelteKit (hooks.server.js)

// src/hooks.server.js
import { redirect } from '@sveltejs/kit'

export async function handle({ event, resolve }) {
  // Disponível em toda request
  const session = await getSession(event.cookies)
  event.locals.user = session?.user

  // Proteger rotas
  if (event.url.pathname.startsWith('/dashboard')) {
    if (!event.locals.user) {
      throw redirect(303, '/login')
    }
  }

  return resolve(event)
}

Renderização

Opções Disponíveis

EstratégiaNext.jsSvelteKit
SSRPadrão (Server Components)Padrão
SSGgenerateStaticParamsprerender = true
ISRrevalidateAdapter-specific
CSR'use client'ssr = false
StreamingSuspensePromises no load

Next.js

// SSG
export async function generateStaticParams() {
  const posts = await getPosts()
  return posts.map(post => ({ slug: post.slug }))
}

// ISR
export const revalidate = 60 // Revalida a cada 60s

// CSR
'use client'
export default function ClientComponent() {
  const [data, setData] = useState(null)
  useEffect(() => { /* fetch */ }, [])
}

SvelteKit

// src/routes/blog/[slug]/+page.js

// SSG
export const prerender = true

// ou dinâmico
export const prerender = false

// CSR only
export const ssr = false

// Geração de rotas estáticas
export async function entries() {
  const posts = await getPosts()
  return posts.map(post => ({ slug: post.slug }))
}

Tabela Comparativa Completa

FeatureNext.js 14+SvelteKit
RoteamentoFile-based (app/)File-based (routes/)
Data FetchingServer Componentsload functions
FormsServer ActionsForm Actions
APIroute.ts+server.js
SSR✅ Padrão✅ Padrão
SSG
ISR✅ NativoVia adapter
Streaming✅ Suspense✅ Promises
Middlewaremiddleware.tshooks.server.js
TypeScript✅ Nativo✅ Nativo
CSSCSS Modules, TailwindScoped nativo, Tailwind
DeployVercel (otimizado)Qualquer (adapters)
Bundle SizeMaiorMenor
Learning CurveMédio-AltoBaixo-Médio

Quando Escolher Cada Um

Next.js

Escolha quando:

  • Time já conhece React
  • Precisa do ecossistema React
  • Deploy na Vercel (DX incrível)
  • Precisa de ISR robusto
  • Projeto muito grande com muitos devs

SvelteKit

Escolha quando:

  • Performance é prioridade
  • Bundle size importa
  • Time pequeno/médio
  • Quer DX mais simples
  • Deploy flexível (qualquer lugar)
  • Projeto novo (greenfield)

✅ Desafio da Aula

Objetivo

Criar uma tabela mental de “tradução” entre os dois frameworks.

Exercício

Complete a tabela (mentalmente ou no papel):

ConceitoNext.jsSvelteKit
Páginapage.tsx?
Layoutlayout.tsx?
Fetch no serverServer Component?
Fetch universal'use client' + useState?
Form submitServer Action?
Redirecionarredirect()?
Erro 404notFound()?
Variável de ambiente públicaNEXT_PUBLIC_*?
Variável privadaNormal?

Respostas

🔍 Clique para ver
ConceitoNext.jsSvelteKit
Páginapage.tsx+page.svelte
Layoutlayout.tsx+layout.svelte
Fetch no serverServer Component+page.server.js load
Fetch universal'use client' + useState+page.js load
Form submitServer ActionForm Action
Redirecionarredirect()throw redirect()
Erro 404notFound()throw error(404)
Env públicaNEXT_PUBLIC_*PUBLIC_*
Env privadaNormalNormal (sem PUBLIC_)

🎉 Conclusão do Módulo 2

Você completou a comparação entre Svelte e React!

Resumo

  • Filosofia: Svelte compila, React usa runtime
  • Reatividade: Atribuição vs Hooks
  • Templates: Blocos declarativos vs JSX
  • Estado: Stores nativos vs Context/Redux
  • CSS: Escopo automático vs CSS-in-JS
  • Performance: Svelte geralmente mais rápido
  • Ecossistema: React maior, Svelte mais curado
  • Meta-frameworks: Ambos excelentes, SvelteKit mais simples

Próximo Passo

Agora que você entende as diferenças, vamos mergulhar profundamente no Svelte a partir do Módulo 3!


Próximo módulo: Módulo 3 — Introdução ao Svelte