TypeScript Svelte Basic
A comprehensive Svelte application archetype built with TypeScript, providing a lightweight, reactive foundation for modern web applications with exceptional performance and developer experience.
Overview
This archetype generates a complete Svelte application using TypeScript for type safety, SvelteKit for full-stack capabilities, and modern tooling for an optimal development experience. Svelte compiles to vanilla JavaScript, resulting in highly optimized, fast-loading applications.
Technology Stack
- Svelte 4+: Reactive framework with compile-time optimizations
- SvelteKit: Full-stack framework for Svelte applications
- TypeScript 5+: Strong typing and modern JavaScript features
- Vite: Ultra-fast build tool and development server
- Vitest: Unit testing framework optimized for Vite
- Playwright: End-to-end testing framework
- ESLint: Code linting and style enforcement
- Prettier: Code formatting and consistency
- Tailwind CSS: Utility-first CSS framework (optional)
Key Features
Core Functionality
- Reactive Programming: Built-in reactivity without virtual DOM
- Component System: Modular, reusable component architecture
- Stores: Reactive state management with built-in stores
- Routing: File-based routing with SvelteKit
- Server-Side Rendering: Optional SSR for better SEO and performance
Development Features
- Hot Module Replacement: Instant updates during development
- TypeScript Integration: Full TypeScript support with type checking
- Component Library: Structured component organization
- Action System: Reusable DOM interaction patterns
- Transition System: Built-in animations and transitions
Production Features
- Bundle Optimization: Tree-shaking and code splitting
- Static Generation: Pre-rendering for optimal performance
- Progressive Enhancement: Works without JavaScript
- Accessibility: Built-in a11y features and warnings
- SEO Optimization: Meta tags and structured data support
Project Structure
{{ project-name }}/
├── src/
│ ├── routes/ # SvelteKit routes (file-based routing)
│ │ ├── +layout.svelte # Root layout
│ │ ├── +page.svelte # Home page
│ │ └── page.svelte.test.ts # Page component tests
│ ├── lib/ # Reusable code and components
│ │ └── index.ts # Library exports
│ ├── app.css # Global styles
│ ├── app.d.ts # Global type declarations
│ ├── app.html # HTML template
│ └── demo.spec.ts # Demo component tests
├── static/ # Static assets
│ ├── app.svg # App logo
│ ├── favicon.png # Favicon
│ ├── globe.svg # Icon assets
│ ├── learn.svg
│ ├── rocket.svg
│ └── ybor-logo.svg
├── e2e/ # End-to-end tests
│ └── demo.test.ts # E2E test specs
├── Dockerfile # Container configuration
├── eslint.config.js # ESLint configuration
├── package.json # Dependencies and scripts
├── playwright.config.ts # Playwright configuration
├── pnpm-lock.yaml # Lock file
├── README.md # Project documentation
├── svelte.config.js # Svelte configuration
├── tsconfig.json # TypeScript configuration
├── vite.config.ts # Vite configuration
└── vitest-setup-client.ts # Vitest client setup
Component Architecture
Basic Component
<!-- UserCard.svelte -->
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Button from '$lib/components/ui/Button.svelte';
import type { User } from '$lib/types/user';
// Props
export let user: User;
export let isFollowing: boolean = false;
export let showActions: boolean = true;
// Event dispatcher
const dispatch = createEventDispatcher<{
follow: User;
unfollow: User;
message: User;
}>();
// Reactive statements
$: displayName = `${user.firstName} ${user.lastName}`;
$: followerText = user.followersCount === 1 ? 'follower' : 'followers';
// Functions
function handleFollow() {
if (isFollowing) {
dispatch('unfollow', user);
} else {
dispatch('follow', user);
}
}
function handleMessage() {
dispatch('message', user);
}
</script>
<article class="user-card">
<header class="user-card__header">
{#if user.avatarUrl}
<img src={user.avatarUrl} alt="{displayName} avatar" class="user-card__avatar" />
{:else}
<div class="user-card__avatar user-card__avatar--placeholder">
{user.firstName[0]}{user.lastName[0]}
</div>
{/if}
<div class="user-card__info">
<h3 class="user-card__name">{displayName}</h3>
<p class="user-card__email">{user.email}</p>
</div>
</header>
{#if user.bio}
<p class="user-card__bio">{user.bio}</p>
{/if}
<div class="user-card__stats">
<span class="stat">
<strong>{user.postsCount}</strong> posts
</span>
<span class="stat">
<strong>{user.followersCount}</strong> {followerText}
</span>
</div>
{#if showActions}
<footer class="user-card__actions">
<Button
variant={isFollowing ? 'outline' : 'primary'}
on:click={handleFollow}
>
{isFollowing ? 'Unfollow' : 'Follow'}
</Button>
<Button variant="outline" on:click={handleMessage}>
Message
</Button>
</footer>
{/if}
</article>
<style>
.user-card {
@apply bg-white rounded-lg shadow-md p-6 max-w-sm;
}
.user-card__header {
@apply flex items-center space-x-4 mb-4;
}
.user-card__avatar {
@apply w-12 h-12 rounded-full object-cover;
}
.user-card__avatar--placeholder {
@apply bg-gray-300 flex items-center justify-center text-gray-600 font-medium;
}
.user-card__name {
@apply text-lg font-semibold text-gray-900;
}
.user-card__email {
@apply text-sm text-gray-600;
}
.user-card__bio {
@apply text-gray-700 mb-4;
}
.user-card__stats {
@apply flex space-x-4 mb-4 text-sm;
}
.stat {
@apply text-gray-600;
}
.user-card__actions {
@apply flex space-x-2;
}
</style>
Store Management
// stores/user.ts
import { writable, derived } from 'svelte/store';
import type { User } from '$lib/types/user';
// User store
function createUserStore() {
const { subscribe, set, update } = writable<User[]>([]);
return {
subscribe,
set,
add: (user: User) => update(users => [...users, user]),
remove: (id: string) => update(users => users.filter(u => u.id !== id)),
update: (id: string, updatedUser: Partial<User>) =>
update(users => users.map(u => u.id === id ? { ...u, ...updatedUser } : u)),
reset: () => set([])
};
}
export const users = createUserStore();
// Derived stores
export const userCount = derived(users, $users => $users.length);
export const activeUsers = derived(users, $users => $users.filter(u => u.isActive));
// Auth store
function createAuthStore() {
const { subscribe, set, update } = writable<{
user: User | null;
token: string | null;
isAuthenticated: boolean;
}>({
user: null,
token: null,
isAuthenticated: false
});
return {
subscribe,
login: (user: User, token: string) => {
set({ user, token, isAuthenticated: true });
localStorage.setItem('auth_token', token);
},
logout: () => {
set({ user: null, token: null, isAuthenticated: false });
localStorage.removeItem('auth_token');
},
updateUser: (updatedUser: Partial<User>) =>
update(auth => ({
...auth,
user: auth.user ? { ...auth.user, ...updatedUser } : null
}))
};
}
export const auth = createAuthStore();
Form Handling
<!-- UserForm.svelte -->
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import Input from '$lib/components/ui/Input.svelte';
import Button from '$lib/components/ui/Button.svelte';
import { validateEmail, validateRequired } from '$lib/utils/validation';
import type { CreateUserRequest } from '$lib/types/user';
export let initialData: Partial<CreateUserRequest> = {};
export let isLoading: boolean = false;
const dispatch = createEventDispatcher<{
submit: CreateUserRequest;
cancel: void;
}>();
// Form data
let formData: CreateUserRequest = {
firstName: initialData.firstName || '',
lastName: initialData.lastName || '',
email: initialData.email || '',
bio: initialData.bio || ''
};
// Validation errors
let errors: Record<string, string> = {};
// Reactive validation
$: {
errors = {};
if (!validateRequired(formData.firstName)) {
errors.firstName = 'First name is required';
}
if (!validateRequired(formData.lastName)) {
errors.lastName = 'Last name is required';
}
if (!validateRequired(formData.email)) {
errors.email = 'Email is required';
} else if (!validateEmail(formData.email)) {
errors.email = 'Please enter a valid email address';
}
}
$: isFormValid = Object.keys(errors).length === 0 &&
formData.firstName &&
formData.lastName &&
formData.email;
function handleSubmit() {
if (isFormValid && !isLoading) {
dispatch('submit', formData);
}
}
function handleCancel() {
dispatch('cancel');
}
function handleReset() {
formData = {
firstName: '',
lastName: '',
email: '',
bio: ''
};
}
</script>
<form on:submit|preventDefault={handleSubmit} class="user-form">
<div class="form-row">
<Input
label="First Name"
bind:value={formData.firstName}
error={errors.firstName}
required
placeholder="Enter first name"
/>
<Input
label="Last Name"
bind:value={formData.lastName}
error={errors.lastName}
required
placeholder="Enter last name"
/>
</div>
<Input
label="Email"
type="email"
bind:value={formData.email}
error={errors.email}
required
placeholder="Enter email address"
/>
<Input
label="Bio"
type="textarea"
bind:value={formData.bio}
placeholder="Tell us about yourself (optional)"
rows={3}
/>
<div class="form-actions">
<Button variant="outline" on:click={handleCancel}>
Cancel
</Button>
<Button variant="outline" on:click={handleReset}>
Reset
</Button>
<Button
type="submit"
variant="primary"
disabled={!isFormValid || isLoading}
loading={isLoading}
>
Create User
</Button>
</div>
</form>
<style>
.user-form {
@apply space-y-4 max-w-md;
}
.form-row {
@apply grid grid-cols-2 gap-4;
}
.form-actions {
@apply flex justify-end space-x-2 pt-4;
}
</style>
API Integration
// services/userService.ts
import { apiClient } from './api';
import type { User, CreateUserRequest, UpdateUserRequest } from '$lib/types/user';
export const userService = {
async getUsers(): Promise<User[]> {
const response = await apiClient.get<{ data: User[] }>('/users');
return response.data.data;
},
async getUserById(id: string): Promise<User> {
const response = await apiClient.get<{ data: User }>(`/users/${id}`);
return response.data.data;
},
async createUser(userData: CreateUserRequest): Promise<User> {
const response = await apiClient.post<{ data: User }>('/users', userData);
return response.data.data;
},
async updateUser(id: string, userData: UpdateUserRequest): Promise<User> {
const response = await apiClient.put<{ data: User }>(`/users/${id}`, userData);
return response.data.data;
},
async deleteUser(id: string): Promise<void> {
await apiClient.delete(`/users/${id}`);
}
};
// Base API client
// services/api.ts
import { browser } from '$app/environment';
const API_BASE_URL = 'http://localhost:5000/api';
class ApiClient {
private baseURL: string;
constructor(baseURL: string) {
this.baseURL = baseURL;
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<{ data: T; status: number }> {
const url = `${this.baseURL}${endpoint}`;
// Get auth token from localStorage (only in browser)
const token = browser ? localStorage.getItem('auth_token') : null;
const config: RequestInit = {
headers: {
'Content-Type': 'application/json',
...(token && { Authorization: `Bearer ${token}` }),
...options.headers,
},
...options,
};
try {
const response = await fetch(url, config);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return {
data,
status: response.status
};
} catch (error) {
console.error('API request failed:', error);
throw error;
}
}
async get<T>(endpoint: string): Promise<{ data: T; status: number }> {
return this.request<T>(endpoint, { method: 'GET' });
}
async post<T>(endpoint: string, data?: unknown): Promise<{ data: T; status: number }> {
return this.request<T>(endpoint, {
method: 'POST',
body: data ? JSON.stringify(data) : undefined,
});
}
async put<T>(endpoint: string, data?: unknown): Promise<{ data: T; status: number }> {
return this.request<T>(endpoint, {
method: 'PUT',
body: data ? JSON.stringify(data) : undefined,
});
}
async delete<T>(endpoint: string): Promise<{ data: T; status: number }> {
return this.request<T>(endpoint, { method: 'DELETE' });
}
}
export const apiClient = new ApiClient(API_BASE_URL);
Testing Strategy
Unit Testing with Vitest
// tests/unit/UserCard.test.ts
import { render, fireEvent } from '@testing-library/svelte';
import { vi } from 'vitest';
import UserCard from '$lib/components/UserCard.svelte';
import type { User } from '$lib/types/user';
const mockUser: User = {
id: '1',
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com',
bio: 'Software developer',
postsCount: 5,
followersCount: 10,
isActive: true
};
describe('UserCard', () => {
it('renders user information correctly', () => {
const { getByText } = render(UserCard, {
props: { user: mockUser }
});
expect(getByText('John Doe')).toBeInTheDocument();
expect(getByText('john.doe@example.com')).toBeInTheDocument();
expect(getByText('Software developer')).toBeInTheDocument();
expect(getByText('5 posts')).toBeInTheDocument();
expect(getByText('10 followers')).toBeInTheDocument();
});
it('dispatches follow event when follow button is clicked', async () => {
const { component, getByText } = render(UserCard, {
props: { user: mockUser, isFollowing: false }
});
const mockHandler = vi.fn();
component.$on('follow', mockHandler);
const followButton = getByText('Follow');
await fireEvent.click(followButton);
expect(mockHandler).toHaveBeenCalledWith(
expect.objectContaining({
detail: mockUser
})
);
});
it('shows unfollow button when user is being followed', () => {
const { getByText } = render(UserCard, {
props: { user: mockUser, isFollowing: true }
});
expect(getByText('Unfollow')).toBeInTheDocument();
});
it('hides actions when showActions is false', () => {
const { queryByText } = render(UserCard, {
props: { user: mockUser, showActions: false }
});
expect(queryByText('Follow')).not.toBeInTheDocument();
expect(queryByText('Message')).not.toBeInTheDocument();
});
});
Store Testing
// tests/unit/stores/user.test.ts
import { get } from 'svelte/store';
import { users, userCount } from '$lib/stores/user';
import type { User } from '$lib/types/user';
const mockUser: User = {
id: '1',
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com',
postsCount: 5,
followersCount: 10,
isActive: true
};
describe('User Store', () => {
beforeEach(() => {
users.reset();
});
it('should add a user', () => {
users.add(mockUser);
const currentUsers = get(users);
expect(currentUsers).toHaveLength(1);
expect(currentUsers[0]).toEqual(mockUser);
});
it('should remove a user', () => {
users.add(mockUser);
users.remove(mockUser.id);
const currentUsers = get(users);
expect(currentUsers).toHaveLength(0);
});
it('should update user count', () => {
users.add(mockUser);
const count = get(userCount);
expect(count).toBe(1);
});
it('should update a user', () => {
users.add(mockUser);
users.update(mockUser.id, { firstName: 'Jane' });
const currentUsers = get(users);
expect(currentUsers[0].firstName).toBe('Jane');
expect(currentUsers[0].lastName).toBe('Doe'); // Other properties unchanged
});
});
E2E Testing with Playwright
// tests/e2e/user-management.spec.ts
import { test, expect } from '@playwright/test';
test.describe('User Management', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/users');
});
test('should display list of users', async ({ page }) => {
await expect(page.locator('[data-testid="user-list"]')).toBeVisible();
await expect(page.locator('[data-testid="user-card"]')).toHaveCountGreaterThan(0);
});
test('should create a new user', async ({ page }) => {
await page.click('[data-testid="add-user-button"]');
await page.fill('[data-testid="firstName-input"]', 'John');
await page.fill('[data-testid="lastName-input"]', 'Doe');
await page.fill('[data-testid="email-input"]', 'john.doe@example.com');
await page.fill('[data-testid="bio-textarea"]', 'Software developer');
await page.click('[data-testid="submit-button"]');
await expect(page.locator('[data-testid="success-message"]')).toContainText('User created successfully');
await expect(page.locator('[data-testid="user-list"]')).toContainText('John Doe');
});
test('should handle form validation', async ({ page }) => {
await page.click('[data-testid="add-user-button"]');
await page.click('[data-testid="submit-button"]'); // Submit empty form
await expect(page.locator('[data-testid="firstName-error"]')).toContainText('First name is required');
await expect(page.locator('[data-testid="lastName-error"]')).toContainText('Last name is required');
await expect(page.locator('[data-testid="email-error"]')).toContainText('Email is required');
});
});
Deployment
Docker Configuration
# Multi-stage build for Svelte application
FROM node:18-alpine AS build
WORKDIR /app
# Copy package files
COPY package*.json ./
RUN npm ci
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Production stage
FROM node:18-alpine AS production
WORKDIR /app
# Copy built application
COPY --from=build /app/build ./build
COPY --from=build /app/package.json ./
COPY --from=build /app/node_modules ./node_modules
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
# Start the application
CMD ["node", "build"]
Kubernetes Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: svelte-app
labels:
app: svelte-app
spec:
replicas: 3
selector:
matchLabels:
app: svelte-app
template:
metadata:
labels:
app: svelte-app
spec:
containers:
- name: svelte-app
image: your-registry/svelte-app:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: svelte-app-service
spec:
selector:
app: svelte-app
ports:
- port: 80
targetPort: 3000
type: ClusterIP
Quick Start
-
Generate the application:
archetect render git@github.com:p6m-archetypes/typescript-svelte-basic.archetype.git -
Install dependencies:
npm install -
Start development server:
npm run dev -
Access the application:
http://localhost:5173 -
Run tests:
# Unit tests
npm run test
# E2E tests
npm run test:e2e
# Test coverage
npm run test:coverage -
Build for production:
npm run build
Best Practices
Component Design
- Keep components small and focused
- Use TypeScript for better type safety
- Leverage Svelte's reactive statements
- Implement proper prop validation
State Management
- Use stores for shared state
- Keep local state in components when appropriate
- Implement derived stores for computed values
- Use custom stores for complex logic
Performance
- Leverage Svelte's compile-time optimizations
- Use proper key attributes in each blocks
- Implement lazy loading for large components
- Optimize bundle size with proper imports
Accessibility
- Use semantic HTML elements
- Implement proper ARIA attributes
- Test with screen readers
- Ensure keyboard navigation works