Gerenciamento de Rotas no SvelteKit

O SvelteKit utiliza um sistema de roteamento baseado em arquivos, onde a estrutura de diretórios do seu projeto define as URLs da aplicação. Esta abordagem simplifica a organização e o entendimento de como as rotas são mapeadas.

Estrutura Básica de Rotas

  • O diretório src/routes serve como a raiz da sua aplicação.
  • Criar um diretório como src/routes/sobre define a rota /sobre.
  • Para rotas dinâmicas, utilize colchetes: src/routes/blog/[slug] cria uma rota onde [slug] é um parâmetro que pode ser acessado, como em /blog/meu-primeiro-post.

É possível modificar o diretório raiz das rotas através das configurações do projeto.

Cada diretório de rota contém um ou mais arquivos de rota, identificados pelo prefixo +. Algumas regras importantes para esses arquivos incluem:

  • Todos os arquivos podem ser executados no servidor.
  • Todos os arquivos, exceto +server, também são executados no cliente.
  • Arquivos +layout e +error aplicam-se ao diretório onde estão e a todos os seus subdiretórios.

Arquivos de Página: +page

+page.svelte

Um componente +page.svelte define uma página específica na sua aplicação. Por padrão, a renderização inicial ocorre no servidor (SSR), e as navegações subsequentes são tratadas no navegador (CSR).

<!--- file: src/routes/+page.svelte --->
<h1>Bem-vindo ao Meu Site!</h1>
<a href="/about">Conheça a Página Sobre</a>

<!--- file: src/routes/about/+page.svelte --->
<h1>Sobre Nós</h1>
<p>Esta é a página "Sobre". Mais conteúdo em breve...</p>
<a href="/">Voltar à Página Inicial</a>

As páginas podem receber dados de uma função load através da propriedade data.

<!--- file: src/routes/articles/[id]/+page.svelte --->
<script>
  /** @type {{ data: import('./$types').PageData }} */
  let { data } = $props();
</script>

<h1>{data.articleTitle}</h1>
<div>{@html data.articleContent}</div>

O SvelteKit utiliza elementos <a> padrão para navegação entre rotas, sem a necessidade de componentes de link específicos do framwork.

+page.js

Para carregar dados que uma página necessita antes da renderização, adicione um módulo +page.js que exporta uma função load:

/// file: src/routes/articles/[id]/+page.js
import { error } from '@sveltejs/kit';

/** @type {import('./$types').PageLoad} */
export function load({ params }) {
  const articles = {
    'primeiro-artigo': {
      articleTitle: 'Primeiro Artigo no Blog',
      articleContent: '<p>Este é o conteúdo do seu primeiro artigo. Explore mais!</p>'
    },
    'segundo-artigo': {
      articleTitle: 'Aprofundando no SvelteKit',
      articleContent: '<p>Um olhar mais detalhado sobre os recursos do SvelteKit.</p>'
    }
  };

  const article = articles[params.id];

  if (article) {
    return article;
  }

  error(404, 'Artigo não encontrado');
}

Esta função load executa-se tanto no servidor (durante o SSR) quanto no navegador (durante a navegação do cliente). Para detalhes completos da API, consulte a documentação da função load.

Além de load, +page.js pode exportar valores para configurar o comportamento da página:

  • export const prerender = true (ou false, 'auto')
  • export const ssr = true (ou false)
  • export const csr = true (ou false)

+page.server.js

Quando a sua função load precisa ser executada exclusivamente no servidor (por exemplo, para acesso a bancos de dados ou variáveis de ambiente privadas), você pode renomear +page.js para +page.server.js e alterar o tipo de PageLoad para PageServerLoad.

/// file: src/routes/products/[productId]/+page.server.js

// Simula uma função de acesso a banco de dados
const fetchProductFromDB = async (id) => {
  // Em um cenário real, aqui haveria uma chamada a um DB
  const products = {
    'pdt-001': { productName: 'Laptop Ultrabook', price: 1200 },
    'pdt-002': { productName: 'Teclado Mecânico', price: 150 }
  };
  return new Promise(resolve => setTimeout(() => resolve(products[id]), 100));
};

import { error } from '@sveltejs/kit';

/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
  const productInfo = await fetchProductFromDB(params.productId);

  if (productInfo) {
    return productInfo;
  }

  error(404, 'Produto não encontrado');
}

Durante a navegação no cliente, o SvelteKit carregará esses dados do servidor, o que significa que o valor retornado deve ser serializável com devalue.

Assim como +page.js, +page.server.js pode exportar opções de página como prerender, ssr e csr. Além disso, arquivos +page.server.js podem exportar *actions*, que permitem a interação com o servidor através de elementos <form> para gravação de dados.

Tratamento de Erros: +error

Em caso de erro durante a execução de uma função load, o SvelteKit exibe uma página de erro padrão. Você pode personalizar essa página adicionando um arquivo +error.svelte no diretório da rota.

<!--- file: src/routes/blog/[slug]/+error.svelte --->
<script>
  import { page } from '$app/state';
</script>

<h1>Erro {page.status}: {page.error.message}</h1>
<p>Ocorreu um problema ao carregar esta página.</p>

O SvelteKit procura o limite de erro mais próximo "subindo" na árvore de diretórios. Se o arquivo acima não existir, ele tentará src/routes/blog/+error.svelte, depois src/routes/+error.svelte, antes de recorrer à página de erro padrão. Se todas as tentativas falharem (ou se o erro for lançado de uma função load de um +layout raiz), o SvelteKit renderizará uma página de erro estática de fallback, que pode ser personalizada através do arquivo src/error.html.

Um erro lançado dentro de uma função load de um +layout(.server).js será tratado pelo arquivo +error.svelte acima daquele layout na hierarquia, e não ao lado dele.

Erros 404 (rota não encontrada) são tratados por src/routes/+error.svelte ou pela página de erro padrão.

Note que erros que ocorrem no handle ou em manipuladores de requisição +server.js não utilizam +error.svelte.

Layouts Compartilhados: +layout

Para elementos que devem ser persistentes em várias páginas (como um cabeçalho de navegação ou rodapé), layouts são a solução ideal. Em vez de duplicar esses elementos em cada +page.svelte, eles são definidos em um arquivo de layout.

+layout.svelte

Crie src/routes/+layout.svelte para um layout que se aplica a todas as páginas. Um layout básico exige que o componente contenha a tag {@render children()} para onde o conteúdo da página será injetado.

<!--- file: src/routes/+layout.svelte --->
<script>
  let { children } = $props();
</script>

<nav>
  <a href="/">Início</a>
  <a href="/servicos">Serviços</a>
  <a href="/contato">Contato</a>
</nav>

<main>
  {@render children()}
</main>

<footer>
  <p>© 2024 Meu Aplicativo</p>
</footer>

Com este layout, navegar entre /, /servicos e /contato apenas substituirá o conteúdo dentro do <main>, mantendo o <nav> e <footer>.

Layouts podem ser aninhados. Por exemplo, para um conjunto de páginas como /configuracoes/perfil e /configuracoes/notificacoes que compartilham um sub-menu, você pode criar um layout específico para o diretório /configuracoes:

<!--- file: src/routes/configuracoes/+layout.svelte --->
<script>
  /** @type {{ data: import('./$types').LayoutData, children: import('svelte').Snippet }} */
  let { data, children } = $props();
</script>

<h1>Página de Configurações</h1>

<div class="submenu">
  <h2>Navegação de Seções:</h2>
  <ul>
    {#each data.menuItems as item}
      <li><a href="/configuracoes/{item.path}">{item.label}</a></li>
    {/each}
  </ul>
</div>

<div class="content-area">
  {@render children()}
</div>

Por padrão, todo layout herda de seus layouts pai. Em cenários específicos onde isso não é desejado, layouts avançados oferecem mais controle.

+layout.js

Componentes +layout.svelte podem buscar dados através de uma função load definida em +layout.js, similar a como as páginas utilizam +page.js.

/// file: src/routes/configuracoes/+layout.js
/** @type {import('./$types').LayoutLoad} */
export function load() {
  return {
    menuItems: [
      { path: 'perfil', label: 'Meu Perfil' },
      { path: 'notificacoes', label: 'Notificações' },
      { path: 'privacidade', label: 'Privacidade' }
    ]
  };
}

Se +layout.js exportar opções de página (prerender, ssr e csr), elas servirão como valores padrão para todas as páginas filhas. Os dados retornados pela função load de um layout também estão disponíveis para todas as suas páginas filhas.

+layout.server.js

Para executar a função load de um layout exclusivamente no servidor, mova-a para +layout.server.js e altere o tipo de LayoutLoad para LayoutServerLoad.

Similarmente a +layout.js, +layout.server.js pode exportar opções de página: prerender, ssr e csr.

Endpoints de Servidor: +server

Além das páginas, você pode definir rotas de API (ou "endpoints") usando arquivos +server.js, que concedem controle total sobre a resposta HTTP. Esses arquivos exportam funções correspondentes aos verbos HTTP (GET, POST, PATCH, PUT, DELETE, OPTIONS, HEAD). Cada função recebe um parâmetro RequestEvent e deve retornar um objeto Response.

Por exemplo, vamos criar uma rota /api/numero-aleatorio com um manipulador GET:

/// file: src/routes/api/numero-aleatorio/+server.js
import { error, json } from '@sveltejs/kit';

/** @type {import('./$types').RequestHandler} */
export function GET({ url }) {
  const minVal = Number(url.searchParams.get('min') ?? '1');
  const maxVal = Number(url.searchParams.get('max') ?? '100');

  if (isNaN(minVal) || isNaN(maxVal) || minVal > maxVal) {
    error(400, 'Os parâmetros min e max devem ser números válidos, e min não deve ser maior que max.');
  }

  const randomInt = Math.floor(Math.random() * (maxVal - minVal + 1)) + minVal;

  return json({ value: randomInt, min: minVal, max: maxVal });
}

O primeiro argumento de Response pode ser um ReadableStream, o que é útil para transmitir grandes volumes de dados ou implementar Server-Sent Events (exceto em plataformas que bufferizam a resposta, como AWS Lambda).

Para sua conveniência, SvelteKit oferece funções auxiliares como error, redirect e json de @sveltejs/kit.

Quando um erro é lançado (seja por error(...) ou um erro inesperado), a resposta será um JSON de erro ou a página de erro de fallback (personalizável via src/error.html), dependendo do cabeçalho Accept. Componentes +error.svelte não são renderizados neste contexto.

Ao criar manipuladores OPTIONS, esteja ciente de que Vite injeta os cabeçalhos Access-Control-Allow-Origin e Access-Control-Allow-Methods durante o desenvolvimento; eles não aparecerão em produção a menos que você os adicione explicitamente.

Arquivos +layout não afetam arquivos +server.js. Para executar lógica antes de cada requisição do servidor, utilize o handle hook.

Recebendo Dados

Arquivos +server.js com manipuladores POST, PUT, PATCH, DELETE, OPTIONS ou HEAD podem ser usados para criar APIs RESTful.

<!--- file: src/routes/name-combiner/+page.svelte --->
<script>
  let firstName = '';
  let lastName = '';
  let fullName = '';

  async function combineNames() {
    const response = await fetch('/api/combine-names', {
      method: 'POST',
      body: JSON.stringify({ name1: firstName, name2: lastName }),
      headers: {
        'content-type': 'application/json'
      }
    });

    fullName = await response.json();
  }
</script>

<input type="text" bind:value={firstName} placeholder="Primeiro Nome">
<input type="text" bind:value={lastName} placeholder="Sobrenome">

<p>Nome Completo: {fullName}</p>

<button onclick={combineNames}>Combinar</button>

/// file: src/routes/api/combine-names/+server.js
import { json } from '@sveltejs/kit';

/** @type {import('./$types').RequestHandler} */
export async function POST({ request }) {
  const { name1, name2 } = await request.json();
  const combined = `${name1} ${name2}`.trim(); // Remove espaços extras se um dos nomes for vazio
  return json(combined);
}

Para submeter dados do navegador para o servidor, as form actions são geralmente a abordagem preferida e mais robusta.

Se um manipulador GET for exportado, requisições HEAD retornarão o content-length do corpo da resposta do manipulador GET.

Manipulador de Fallback

A exportação de um manipulador fallback capturará qualquer método de requisição HTTP não explicitamente tratado, incluindo métodos menos comuns como MOVE.

/// file: src/routes/api/generic-handler/+server.js
import { json, text } from '@sveltejs/kit';

export async function POST({ request }) {
  const data = await request.json();
  return json({ received: data, method: 'POST' });
}

/** @type {import('./$types').RequestHandler} */
export async function fallback({ request }) {
  return text(`Requisição ${request.method} recebida e tratada pelo fallback!`);
}

Para requisições HEAD, o manipulador GET tem precedência sobre o manipulador fallback.

Negociação de Conteúdo

É possível ter arquivos +server.js e +page no mesmo diretório, permitindo que a mesma rota funcione como página e como endpoint de API. O SvelteKit aplica as seguintes regras para determinar qual usar:

  • Requisições PUT, PATCH, DELETE e OPTIONS são sempre tratadas por +server.js, pois não são aplicáveis a páginas.
  • Requisições GET, POST e HEAD são consideradas requisições de página se o cabeçalho Accept priorizar text/html (típico de navegadores). Caso contrário, são tratadas por +server.js.
  • Respostas a requisições GET incluirão o cabeçalho Vary: Accept para permitir que proxies e navegadores cacheiem separadamente as respostas HTML e JSON.

Tipagem com $types

Para desenvolvedores TypeScript (ou JavaScript com JSDoc), o SvelteKit gera um arquivo $types.d.ts em um diretório oculto, fornecendo segurança de tipo ao manipular arquivos de rota. Isso inclui tipos para:

  • PageData (para +page.svelte) e LayoutData (para +layout.svelte) para tipar a propriedade data.
  • PageLoad, PageServerLoad, LayoutLoad e LayoutServerLoad para garantir que os parâmetros e valores de retorno das funções load estejam corretamente tipados.
<!--- file: src/routes/blog/[slug]/+page.svelte --->
<script>
  /** @type {{ data: import('./$types').PageData }} */
  let { data } = $props();
</script>

Ferramentas de IDE para Svelte (como a extensão Svelte for VS Code) podem inferir e inserir esses tipos automaticamente, eliminando a necessidade de escrevê-los manualmente e garantindo a verificação de tipos. O mesmo se aplica ao svelte-check.

Outros Arquivos

Qualquer outro arquivo dentro de um diretório de rota é ignorado pelo SvelteKit. Isso permite que você coloque componentes e módulos de utilidade juntamente com as rotas que os utilizam. Se componentes ou módulos forem necessários por múltiplas rotas, a convenção é armazená-los no diretório $lib.

Tags: SvelteKit Routing SSR CSR API Routes

Publicado em 10-1 19:32