Skip to main content

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

  1. Generate the application:

    archetect render git@github.com:p6m-archetypes/typescript-svelte-basic.archetype.git
  2. Install dependencies:

    npm install
  3. Start development server:

    npm run dev
  4. Access the application:

    http://localhost:5173
  5. Run tests:

    # Unit tests
    npm run test

    # E2E tests
    npm run test:e2e

    # Test coverage
    npm run test:coverage
  6. 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