Prerequisites
- Basic understanding of JavaScript π
- TypeScript installation β‘
- VS Code or preferred IDE π»
What you'll learn
- Understand SvelteKit fundamentals π―
- Apply SvelteKit in real projects ποΈ
- Debug common SvelteKit issues π
- Write type-safe code β¨
π― Introduction
Welcome to the exciting world of SvelteKit with TypeScript! π In this comprehensive guide, weβll explore how to build full-stack applications with SvelteKit and TypeScript, combining the best of both worlds: Svelteβs simplicity and TypeScriptβs type safety.
Youβll discover how SvelteKit can transform your web development experience. Whether youβre building server-side rendered applications π, static sites π, or full-stack web apps π₯οΈ, understanding SvelteKit with TypeScript is essential for creating robust, maintainable, and blazingly fast applications.
By the end of this tutorial, youβll feel confident building full-stack applications with SvelteKit and TypeScript! Letβs dive in! πββοΈ
π Understanding SvelteKit
π€ What is SvelteKit?
SvelteKit is like having a Swiss Army knife for web development π¨. Think of it as a meta-framework that combines the compile-time magic of Svelte with full-stack capabilities, providing everything you need to build modern web applications.
In TypeScript terms, SvelteKit provides a complete framework with built-in TypeScript support, file-based routing, server-side rendering, and API routes. This means you can:
- β¨ Build both frontend and backend in one project
- π Get incredible performance with compile-time optimizations
- π‘οΈ Enjoy full type safety across your entire application
π‘ Why Use SvelteKit with TypeScript?
Hereβs why developers love this combination:
- Type Safety π: Catch errors at compile-time across your entire stack
- Better IDE Support π»: Amazing autocomplete and refactoring capabilities
- Code Documentation π: Types serve as living documentation
- Performance β‘: Compile-time optimizations with runtime type checking
- Developer Experience π―: Hot module replacement and excellent debugging
Real-world example: Imagine building a blog platform π. With SvelteKit and TypeScript, you can have type-safe API routes, components, and even database queries all in one cohesive project!
π§ Basic Setup and Project Structure
π Creating Your First SvelteKit Project
Letβs start by setting up a new SvelteKit project with TypeScript:
# π Create a new SvelteKit project
npm create svelte@latest my-sveltekit-app
# π Navigate to the project
cd my-sveltekit-app
# π¦ Install dependencies
npm install
When prompted, choose:
- β TypeScript support
- β ESLint for code quality
- β Prettier for formatting
- β Playwright for end-to-end testing (optional)
ποΈ Project Structure Overview
Hereβs what your SvelteKit TypeScript project looks like:
my-sveltekit-app/
βββ src/
β βββ lib/ # π Shared components and utilities
β βββ routes/ # π£οΈ File-based routing system
β βββ app.html # π Main HTML template
β βββ app.d.ts # π― TypeScript declarations
βββ static/ # π Static assets
βββ svelte.config.js # βοΈ Svelte configuration
βββ tsconfig.json # π§ TypeScript configuration
βββ vite.config.js # β‘ Vite build configuration
π‘ Pro Tip: The src/routes
directory is where the magic happens! Each file becomes a route automatically.
π‘ Practical Examples
π Example 1: Building a Type-Safe Home Page
Letβs create a home page with TypeScript:
<!-- src/routes/+page.svelte -->
<script lang="ts">
// π― Define our data types
interface WelcomeData {
title: string;
description: string;
features: Feature[];
}
interface Feature {
id: number;
name: string;
emoji: string;
description: string;
}
// ποΈ Our welcome data
const welcomeData: WelcomeData = {
title: "Welcome to SvelteKit! π",
description: "Build amazing full-stack apps with TypeScript",
features: [
{
id: 1,
name: "Type Safety",
emoji: "π‘οΈ",
description: "Catch errors before they reach production"
},
{
id: 2,
name: "Performance",
emoji: "β‘",
description: "Blazingly fast with compile-time optimizations"
},
{
id: 3,
name: "Developer Experience",
emoji: "π―",
description: "Amazing tooling and hot module replacement"
}
]
};
// π¨ Handle feature clicks
const handleFeatureClick = (feature: Feature): void => {
console.log(`β¨ Clicked on ${feature.name}!`);
};
</script>
<main class="container">
<h1>{welcomeData.title}</h1>
<p class="description">{welcomeData.description}</p>
<div class="features">
{#each welcomeData.features as feature (feature.id)}
<div
class="feature-card"
on:click={() => handleFeatureClick(feature)}
on:keydown={() => handleFeatureClick(feature)}
>
<span class="emoji">{feature.emoji}</span>
<h3>{feature.name}</h3>
<p>{feature.description}</p>
</div>
{/each}
</div>
</main>
<style>
.container {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-top: 2rem;
}
.feature-card {
background: #f8f9fa;
border-radius: 8px;
padding: 1.5rem;
cursor: pointer;
transition: transform 0.2s;
}
.feature-card:hover {
transform: translateY(-2px);
}
.emoji {
font-size: 2rem;
}
</style>
π― Try it yourself: Add a new feature to the array and see the type safety in action!
π Example 2: Creating API Routes with TypeScript
Letβs build a shopping cart API:
// src/routes/api/cart/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
// ποΈ Define our cart item type
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
emoji: string;
}
// ποΈ In-memory storage (use a real database in production!)
let cartItems: CartItem[] = [
{ id: '1', name: 'TypeScript Book', price: 29.99, quantity: 1, emoji: 'π' },
{ id: '2', name: 'SvelteKit Course', price: 49.99, quantity: 1, emoji: 'π' }
];
// π GET: Retrieve all cart items
export const GET: RequestHandler = async () => {
return json({
success: true,
items: cartItems,
total: cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0)
});
};
// β POST: Add item to cart
export const POST: RequestHandler = async ({ request }) => {
try {
const newItem: Omit<CartItem, 'id'> = await request.json();
// π― Generate ID and add to cart
const cartItem: CartItem = {
...newItem,
id: Date.now().toString()
};
cartItems.push(cartItem);
return json({
success: true,
message: `Added ${newItem.emoji} ${newItem.name} to cart! π`,
item: cartItem
});
} catch (error) {
return json({
success: false,
message: 'Failed to add item to cart π'
}, { status: 400 });
}
};
// ποΈ DELETE: Remove item from cart
export const DELETE: RequestHandler = async ({ url }) => {
const itemId = url.searchParams.get('id');
if (!itemId) {
return json({
success: false,
message: 'Item ID is required π'
}, { status: 400 });
}
const initialLength = cartItems.length;
cartItems = cartItems.filter(item => item.id !== itemId);
if (cartItems.length < initialLength) {
return json({
success: true,
message: 'Item removed from cart! ποΈ'
});
} else {
return json({
success: false,
message: 'Item not found π€·ββοΈ'
}, { status: 404 });
}
};
π Example 3: Using the API in a Component
Now letβs create a component that uses our API:
<!-- src/routes/cart/+page.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
// π― Define our types
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
emoji: string;
}
interface CartResponse {
success: boolean;
items: CartItem[];
total: number;
}
// π Component state
let cartItems: CartItem[] = [];
let total: number = 0;
let loading: boolean = true;
let error: string | null = null;
// π Load cart items
const loadCart = async (): Promise<void> => {
try {
loading = true;
const response = await fetch('/api/cart');
const data: CartResponse = await response.json();
if (data.success) {
cartItems = data.items;
total = data.total;
error = null;
} else {
error = 'Failed to load cart items π';
}
} catch (e) {
error = 'Network error occurred π';
console.error('Cart loading error:', e);
} finally {
loading = false;
}
};
// ποΈ Remove item from cart
const removeItem = async (itemId: string): Promise<void> => {
try {
const response = await fetch(`/api/cart?id=${itemId}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
await loadCart(); // Reload cart
console.log('β
Item removed successfully!');
} else {
error = result.message;
}
} catch (e) {
error = 'Failed to remove item π';
console.error('Remove error:', e);
}
};
// π Load cart on component mount
onMount(() => {
loadCart();
});
</script>
<div class="cart-container">
<h1>π Your Shopping Cart</h1>
{#if loading}
<div class="loading">Loading your cart... β³</div>
{:else if error}
<div class="error">
{error}
<button on:click={loadCart}>Try Again π</button>
</div>
{:else if cartItems.length === 0}
<div class="empty-cart">
<p>Your cart is empty! π</p>
<a href="/">Start Shopping ποΈ</a>
</div>
{:else}
<div class="cart-items">
{#each cartItems as item (item.id)}
<div class="cart-item">
<span class="emoji">{item.emoji}</span>
<div class="item-details">
<h3>{item.name}</h3>
<p>Quantity: {item.quantity}</p>
<p class="price">${item.price.toFixed(2)}</p>
</div>
<button
class="remove-btn"
on:click={() => removeItem(item.id)}
>
Remove ποΈ
</button>
</div>
{/each}
</div>
<div class="cart-total">
<h2>Total: ${total.toFixed(2)} π°</h2>
<button class="checkout-btn">Checkout π</button>
</div>
{/if}
</div>
<style>
.cart-container {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
}
.cart-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
border: 1px solid #ddd;
border-radius: 8px;
margin-bottom: 1rem;
}
.emoji {
font-size: 2rem;
}
.item-details {
flex: 1;
}
.remove-btn {
background: #ff4444;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
}
.cart-total {
text-align: center;
margin-top: 2rem;
padding: 1rem;
background: #f8f9fa;
border-radius: 8px;
}
.checkout-btn {
background: #28a745;
color: white;
border: none;
padding: 1rem 2rem;
border-radius: 8px;
font-size: 1.1rem;
cursor: pointer;
margin-top: 1rem;
}
.loading, .error, .empty-cart {
text-align: center;
padding: 2rem;
}
.error {
color: #dc3545;
}
</style>
π Advanced Concepts
π§ββοΈ Advanced Topic 1: Server-Side Rendering with Load Functions
When youβre ready to level up, try server-side data loading:
// src/routes/products/+page.server.ts
import type { PageServerLoad } from './$types';
// π― Product interface
interface Product {
id: string;
name: string;
price: number;
description: string;
emoji: string;
inStock: boolean;
}
// π Server-side data loading
export const load: PageServerLoad = async ({ fetch, url }) => {
// π Get search parameters
const category = url.searchParams.get('category') || 'all';
const sort = url.searchParams.get('sort') || 'name';
// ποΈ Simulate API call (replace with real API)
const products: Product[] = [
{
id: '1',
name: 'TypeScript Handbook',
price: 39.99,
description: 'The definitive guide to TypeScript',
emoji: 'π',
inStock: true
},
{
id: '2',
name: 'SvelteKit Masterclass',
price: 89.99,
description: 'Master full-stack development with SvelteKit',
emoji: 'π',
inStock: true
},
{
id: '3',
name: 'Web Performance Kit',
price: 59.99,
description: 'Tools and techniques for lightning-fast websites',
emoji: 'β‘',
inStock: false
}
];
// π¨ Filter and sort products
let filteredProducts = products;
if (category !== 'all') {
filteredProducts = products.filter(p =>
p.name.toLowerCase().includes(category.toLowerCase())
);
}
// π Sort products
filteredProducts.sort((a, b) => {
switch (sort) {
case 'price':
return a.price - b.price;
case 'name':
return a.name.localeCompare(b.name);
default:
return 0;
}
});
return {
products: filteredProducts,
category,
sort,
totalProducts: products.length
};
};
ποΈ Advanced Topic 2: Type-Safe Stores
For state management across your app:
// src/lib/stores/cart.ts
import { writable, derived } from 'svelte/store';
// ποΈ Cart item type
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
emoji: string;
}
// π Cart store type
interface CartStore {
items: CartItem[];
loading: boolean;
error: string | null;
}
// ποΈ Create cart store
const createCartStore = () => {
const { subscribe, set, update } = writable<CartStore>({
items: [],
loading: false,
error: null
});
return {
subscribe,
// β Add item to cart
addItem: (item: Omit<CartItem, 'id'>) => {
update(store => {
const cartItem: CartItem = { ...item, id: Date.now().toString() };
return {
...store,
items: [...store.items, cartItem],
error: null
};
});
},
// ποΈ Remove item from cart
removeItem: (id: string) => {
update(store => ({
...store,
items: store.items.filter(item => item.id !== id)
}));
},
// π§Ή Clear cart
clear: () => {
update(store => ({
...store,
items: []
}));
},
// π Set loading state
setLoading: (loading: boolean) => {
update(store => ({ ...store, loading }));
},
// β οΈ Set error
setError: (error: string | null) => {
update(store => ({ ...store, error }));
}
};
};
// π― Export the cart store
export const cart = createCartStore();
// π° Derived store for cart total
export const cartTotal = derived(cart, $cart =>
$cart.items.reduce((sum, item) => sum + (item.price * item.quantity), 0)
);
// π Derived store for item count
export const cartItemCount = derived(cart, $cart =>
$cart.items.reduce((sum, item) => sum + item.quantity, 0)
);
β οΈ Common Pitfalls and Solutions
π± Pitfall 1: Missing Type Annotations
// β Wrong way - TypeScript can't infer the type!
const handleSubmit = async (event) => {
event.preventDefault(); // π₯ Error: Parameter 'event' implicitly has an 'any' type
const formData = new FormData(event.target);
};
// β
Correct way - proper type annotations!
const handleSubmit = async (event: SubmitEvent) => {
event.preventDefault(); // β
TypeScript knows this is a SubmitEvent
const target = event.target as HTMLFormElement;
const formData = new FormData(target);
};
π€― Pitfall 2: Incorrect API Response Types
// β Dangerous - assuming response structure!
const loadData = async () => {
const response = await fetch('/api/data');
const data = await response.json(); // π₯ 'data' is of type 'any'
return data.items.map(item => item.name); // π« No type safety!
};
// β
Safe - define response types!
interface ApiResponse {
success: boolean;
items: Array<{ id: string; name: string; emoji: string }>;
error?: string;
}
const loadData = async (): Promise<string[]> => {
const response = await fetch('/api/data');
const data: ApiResponse = await response.json();
if (!data.success) {
throw new Error(data.error || 'Failed to load data');
}
return data.items.map(item => `${item.emoji} ${item.name}`); // β
Type safe!
};
π οΈ Best Practices
- π― Use TypeScript Everywhere: Enable TypeScript in all
.svelte
files with<script lang="ts">
- π Define Clear Interfaces: Create interfaces for your data structures
- π‘οΈ Validate API Responses: Donβt trust external data - validate it!
- π¨ Leverage SvelteKit Features: Use load functions for server-side data
- β¨ Use Derived Stores: Create reactive computed values with derived stores
- π§ Configure TypeScript Strictly: Enable strict mode for better type checking
π§ͺ Hands-On Exercise
π― Challenge: Build a Blog System
Create a type-safe blog system with SvelteKit:
π Requirements:
- β Blog posts with title, content, author, and date
- π·οΈ Categories and tags for posts
- π€ Author profiles with bio and avatar
- π Publication dates with proper formatting
- π¨ Each post needs an emoji category!
π Bonus Points:
- Add server-side search functionality
- Implement pagination with proper types
- Create a comment system
- Add markdown support for post content
π‘ Solution
π Click to see solution
// src/lib/types/blog.ts
export interface BlogPost {
id: string;
title: string;
content: string;
excerpt: string;
author: Author;
publishedAt: Date;
updatedAt: Date;
category: PostCategory;
tags: string[];
emoji: string;
published: boolean;
}
export interface Author {
id: string;
name: string;
email: string;
bio: string;
avatar: string;
socialLinks: SocialLinks;
}
export interface SocialLinks {
twitter?: string;
github?: string;
linkedin?: string;
website?: string;
}
export type PostCategory = 'tutorial' | 'news' | 'tips' | 'review';
export interface BlogApiResponse {
success: boolean;
posts: BlogPost[];
total: number;
page: number;
totalPages: number;
}
// src/routes/blog/+page.server.ts
import type { PageServerLoad } from './$types';
import type { BlogPost, BlogApiResponse } from '$lib/types/blog';
export const load: PageServerLoad = async ({ url }) => {
const page = parseInt(url.searchParams.get('page') || '1');
const category = url.searchParams.get('category') || 'all';
const search = url.searchParams.get('search') || '';
// ποΈ Simulate API call
const posts: BlogPost[] = [
{
id: '1',
title: 'Getting Started with SvelteKit',
content: 'Learn how to build amazing apps...',
excerpt: 'A beginner-friendly guide to SvelteKit',
author: {
id: '1',
name: 'Sarah Developer',
email: '[email protected]',
bio: 'Full-stack developer passionate about web technologies',
avatar: '/avatars/sarah.jpg',
socialLinks: {
twitter: '@sarahdev',
github: 'sarahdev'
}
},
publishedAt: new Date('2023-01-15'),
updatedAt: new Date('2023-01-16'),
category: 'tutorial',
tags: ['sveltekit', 'beginners', 'javascript'],
emoji: 'π',
published: true
}
];
// π¨ Filter posts
let filteredPosts = posts.filter(post => post.published);
if (category !== 'all') {
filteredPosts = filteredPosts.filter(post => post.category === category);
}
if (search) {
filteredPosts = filteredPosts.filter(post =>
post.title.toLowerCase().includes(search.toLowerCase()) ||
post.content.toLowerCase().includes(search.toLowerCase())
);
}
// π Pagination
const postsPerPage = 10;
const totalPages = Math.ceil(filteredPosts.length / postsPerPage);
const startIndex = (page - 1) * postsPerPage;
const paginatedPosts = filteredPosts.slice(startIndex, startIndex + postsPerPage);
return {
posts: paginatedPosts,
total: filteredPosts.length,
page,
totalPages,
category,
search
};
};
<!-- src/routes/blog/+page.svelte -->
<script lang="ts">
import type { PageData } from './$types';
import { page } from '$app/stores';
import { goto } from '$app/navigation';
export let data: PageData;
// π― Handle search
let searchQuery = data.search || '';
const handleSearch = async () => {
const url = new URL($page.url);
url.searchParams.set('search', searchQuery);
url.searchParams.set('page', '1');
await goto(url.toString());
};
// π·οΈ Handle category filter
const filterByCategory = async (category: string) => {
const url = new URL($page.url);
url.searchParams.set('category', category);
url.searchParams.set('page', '1');
await goto(url.toString());
};
</script>
<div class="blog-container">
<h1>π Our Blog</h1>
<!-- π Search and filters -->
<div class="controls">
<input
type="text"
bind:value={searchQuery}
placeholder="Search posts... π"
on:keydown={(e) => e.key === 'Enter' && handleSearch()}
/>
<button on:click={handleSearch}>Search</button>
<div class="categories">
<button
class:active={data.category === 'all'}
on:click={() => filterByCategory('all')}
>
All π
</button>
<button
class:active={data.category === 'tutorial'}
on:click={() => filterByCategory('tutorial')}
>
Tutorials π
</button>
<button
class:active={data.category === 'tips'}
on:click={() => filterByCategory('tips')}
>
Tips π‘
</button>
</div>
</div>
<!-- π° Blog posts -->
<div class="posts">
{#each data.posts as post (post.id)}
<article class="post-card">
<h2>
<a href="/blog/{post.id}">
{post.emoji} {post.title}
</a>
</h2>
<p class="excerpt">{post.excerpt}</p>
<div class="meta">
<span>By {post.author.name}</span>
<span>{post.publishedAt.toLocaleDateString()}</span>
<span class="category">{post.category}</span>
</div>
<div class="tags">
{#each post.tags as tag}
<span class="tag">#{tag}</span>
{/each}
</div>
</article>
{/each}
</div>
<!-- π Pagination -->
{#if data.totalPages > 1}
<div class="pagination">
{#each Array(data.totalPages) as _, i}
<a
href="/blog?page={i + 1}&category={data.category}&search={data.search}"
class:active={data.page === i + 1}
>
{i + 1}
</a>
{/each}
</div>
{/if}
</div>
π Key Takeaways
Youβve learned so much! Hereβs what you can now do:
- β Set up SvelteKit with TypeScript with confidence πͺ
- β Create type-safe components that are maintainable π‘οΈ
- β Build API routes with proper TypeScript types π―
- β Handle server-side rendering with load functions π
- β Create full-stack applications with SvelteKit! π
Remember: SvelteKit with TypeScript gives you the best of both worlds - Svelteβs simplicity and TypeScriptβs safety. Itβs like having a superpower for web development! π¦ΈββοΈ
π€ Next Steps
Congratulations! π Youβve mastered SvelteKit with TypeScript!
Hereβs what to do next:
- π» Build a complete project using what youβve learned
- ποΈ Explore SvelteKitβs advanced features like hooks and handle
- π Learn about deployment options (Vercel, Netlify, Node.js)
- π Share your SvelteKit creations with the community!
Remember: Every SvelteKit expert started as a beginner. Keep building, keep learning, and most importantly, have fun creating amazing web applications! π
Happy coding with SvelteKit and TypeScript! ππβ¨