Svelte / SvelteKit - Guía de Entorno¶
Guía para configurar el entorno de desarrollo Svelte/SvelteKit para trabajar con Claude Code.
Resumen de Capacidades¶
| Capacidad | Herramientas |
|---|---|
| Framework | Svelte 5, SvelteKit 2 |
| Build | Vite |
| State | Svelte Stores, Runes |
| Testing | Vitest, Playwright |
| Styling | Tailwind, vanilla CSS |
| Type Safety | TypeScript |
Instalación¶
Node.js y pnpm¶
# Windows
winget install OpenJS.NodeJS.LTS
corepack enable
corepack prepare pnpm@latest --activate
# macOS
brew install node@22
corepack enable
# Verificar
node --version # 22.x+
pnpm --version # 9.x+
Crear Proyectos¶
SvelteKit (Full-stack - Recomendado)¶
# Crear proyecto
pnpm create svelte@latest my-sveltekit-app
# Opciones recomendadas:
# - Which template: Skeleton project
# - TypeScript: Yes
# - ESLint: Yes
# - Prettier: Yes
# - Playwright: Yes
# - Vitest: Yes
cd my-sveltekit-app
pnpm install
pnpm dev
Svelte Standalone (Vite)¶
# Solo frontend (sin SSR)
pnpm create vite@latest my-svelte-app -- --template svelte-ts
cd my-svelte-app
pnpm install
pnpm dev
Estructura de Proyecto (SvelteKit)¶
my-sveltekit-app/
├── src/
│ ├── lib/
│ │ ├── components/
│ │ │ └── Button.svelte
│ │ ├── stores/
│ │ │ └── user.ts
│ │ ├── server/
│ │ │ └── db.ts # Solo server-side
│ │ └── utils/
│ │ └── helpers.ts
│ ├── routes/
│ │ ├── +layout.svelte
│ │ ├── +layout.server.ts
│ │ ├── +page.svelte
│ │ ├── +page.ts # Load data
│ │ ├── +page.server.ts # Server-only load
│ │ ├── about/
│ │ │ └── +page.svelte
│ │ └── api/
│ │ └── users/
│ │ └── +server.ts
│ ├── app.html
│ ├── app.css
│ └── app.d.ts
├── static/
├── tests/
├── svelte.config.js
├── vite.config.ts
├── tsconfig.json
└── package.json
Svelte 5 Runes¶
Svelte 5 introduce "runes" como nueva API de reactividad.
$state (Estado reactivo)¶
<script lang="ts">
// Svelte 5 runes
let count = $state(0);
let user = $state({ name: 'John', age: 30 });
function increment() {
count++; // Automáticamente reactivo
}
function updateName(newName: string) {
user.name = newName; // Mutación directa OK
}
</script>
<button onclick={increment}>
Count: {count}
</button>
<p>User: {user.name}, {user.age}</p>
$derived (Valores computados)¶
<script lang="ts">
let count = $state(0);
// Automáticamente recalculado cuando count cambia
let doubled = $derived(count * 2);
// Derived con lógica más compleja
let message = $derived.by(() => {
if (count > 10) return 'High';
if (count > 5) return 'Medium';
return 'Low';
});
</script>
<p>Count: {count}, Doubled: {doubled}, Level: {message}</p>
$effect (Side effects)¶
<script lang="ts">
let count = $state(0);
// Se ejecuta cuando count cambia
$effect(() => {
console.log('Count changed:', count);
// Cleanup (opcional)
return () => {
console.log('Cleanup');
};
});
// Pre-effect (antes del DOM update)
$effect.pre(() => {
console.log('Before DOM update');
});
</script>
$props (Props del componente)¶
<!-- Button.svelte -->
<script lang="ts">
interface Props {
variant?: 'primary' | 'secondary';
disabled?: boolean;
onclick?: () => void;
children: import('svelte').Snippet;
}
let {
variant = 'primary',
disabled = false,
onclick,
children
}: Props = $props();
</script>
<button
class={variant}
{disabled}
{onclick}
>
{@render children()}
</button>
Routing (SvelteKit)¶
Páginas¶
src/routes/
├── +page.svelte → /
├── about/
│ └── +page.svelte → /about
├── blog/
│ ├── +page.svelte → /blog
│ └── [slug]/
│ └── +page.svelte → /blog/:slug
└── users/
└── [...rest]/
└── +page.svelte → /users/*
Load Functions¶
// routes/blog/[slug]/+page.ts
import type { PageLoad } from './$types';
export const load: PageLoad = async ({ params, fetch }) => {
const response = await fetch(`/api/posts/${params.slug}`);
const post = await response.json();
return {
post
};
};
<!-- routes/blog/[slug]/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
let { data }: { data: PageData } = $props();
</script>
<article>
<h1>{data.post.title}</h1>
<div>{@html data.post.content}</div>
</article>
Server Load (con acceso a DB)¶
// routes/dashboard/+page.server.ts
import type { PageServerLoad } from './$types';
import { db } from '$lib/server/db';
import { redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ locals }) => {
if (!locals.user) {
throw redirect(303, '/login');
}
const stats = await db.getStats(locals.user.id);
return {
stats
};
};
API Routes¶
// routes/api/users/+server.ts
import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { db } from '$lib/server/db';
export const GET: RequestHandler = async ({ url }) => {
const limit = Number(url.searchParams.get('limit')) || 10;
const users = await db.getUsers(limit);
return json(users);
};
export const POST: RequestHandler = async ({ request }) => {
const body = await request.json();
if (!body.email) {
throw error(400, 'Email is required');
}
const user = await db.createUser(body);
return json(user, { status: 201 });
};
Stores (Svelte 4 / compatible con 5)¶
// lib/stores/user.ts
import { writable, derived } from 'svelte/store';
interface User {
id: string;
name: string;
email: string;
}
function createUserStore() {
const { subscribe, set, update } = writable<User | null>(null);
return {
subscribe,
login: (user: User) => set(user),
logout: () => set(null),
updateName: (name: string) =>
update(u => u ? { ...u, name } : null)
};
}
export const user = createUserStore();
// Derived store
export const isAuthenticated = derived(user, $user => !!$user);
<script lang="ts">
import { user, isAuthenticated } from '$lib/stores/user';
</script>
{#if $isAuthenticated}
<p>Welcome, {$user?.name}</p>
<button onclick={() => user.logout()}>Logout</button>
{:else}
<a href="/login">Login</a>
{/if}
Testing¶
Unit Tests (Vitest)¶
// tests/components/Button.test.ts
import { render, screen, fireEvent } from '@testing-library/svelte';
import { describe, it, expect, vi } from 'vitest';
import Button from '$lib/components/Button.svelte';
describe('Button', () => {
it('renders with text', () => {
render(Button, { props: { children: 'Click me' } });
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onclick when clicked', async () => {
const handleClick = vi.fn();
render(Button, { props: { onclick: handleClick } });
await fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalled();
});
});
E2E Tests (Playwright)¶
// tests/home.test.ts
import { expect, test } from '@playwright/test';
test('home page has welcome message', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toContainText('Welcome');
});
test('navigation works', async ({ page }) => {
await page.goto('/');
await page.click('[data-testid="nav-about"]');
await expect(page).toHaveURL('/about');
});
Forms y Actions¶
<!-- routes/login/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
import type { ActionData } from './$types';
let { form }: { form: ActionData } = $props();
</script>
<form method="POST" use:enhance>
<input name="email" type="email" required />
<input name="password" type="password" required />
{#if form?.error}
<p class="error">{form.error}</p>
{/if}
<button type="submit">Login</button>
</form>
// routes/login/+page.server.ts
import { fail, redirect } from '@sveltejs/kit';
import type { Actions } from './$types';
import { db } from '$lib/server/db';
export const actions: Actions = {
default: async ({ request, cookies }) => {
const data = await request.formData();
const email = data.get('email') as string;
const password = data.get('password') as string;
const user = await db.authenticate(email, password);
if (!user) {
return fail(400, { error: 'Invalid credentials' });
}
cookies.set('session', user.sessionId, { path: '/' });
throw redirect(303, '/dashboard');
}
};
Styling¶
Global CSS¶
/* src/app.css */
:root {
--primary: #ff3e00;
--bg: #ffffff;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a1a;
}
}
Scoped Styles¶
<style>
/* Automáticamente scoped al componente */
.card {
background: var(--bg);
padding: 1rem;
border-radius: 8px;
}
/* Global dentro de componente */
:global(.external-lib-class) {
color: var(--primary);
}
</style>
Tailwind CSS¶
// tailwind.config.js
export default {
content: ['./src/**/*.{html,js,svelte,ts}'],
theme: {
extend: {}
},
plugins: []
};
Comandos que Claude Code Ejecutará¶
# Crear proyecto
pnpm create svelte@latest my-app
pnpm create vite@latest my-app -- --template svelte-ts
# Desarrollo
pnpm dev
pnpm build
pnpm preview
# Testing
pnpm test
pnpm test:unit
pnpm test:e2e
# Linting
pnpm lint
pnpm check # svelte-check
# Dependencias
pnpm add package
pnpm add -D dev-package
VS Code Extensions¶
code --install-extension svelte.svelte-vscode
code --install-extension dbaeumer.vscode-eslint
code --install-extension esbenp.prettier-vscode
code --install-extension bradlc.vscode-tailwindcss
Verificación del Entorno¶
#!/bin/bash
echo "=== Verificación Entorno Svelte ==="
echo -e "\n--- Runtime ---"
node --version
pnpm --version
echo -e "\n--- Svelte ---"
pnpm dlx svelte --version 2>/dev/null || echo "Svelte via create-svelte"
echo -e "\n=== Verificación Completa ==="