Basic Example
A complete walkthrough of creating your first executable SvelteKit application.
Project Overview
We’ll build a simple note-taking application that demonstrates:
- Static file serving
- Local data persistence
- Simple API routes
- Client-side interactivity
Project Setup
1. Initialize the Project
# Create new SvelteKit project
npm create svelte@latest my-notes-app
cd my-notes-app
npm install
# Install the exec adapter
npm install -D sveltekit-exec-adapter
2. Configure the Adapter
// svelte.config.js
import adapter from 'sveltekit-exec-adapter';
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter({
embedStatic: true,
validation: {
maxAssetSize: 10 * 1024 * 1024, // 10MB
allowedExtensions: ['.css', '.js', '.png', '.svg', '.ico']
}
}),
prerender: {
entries: []
}
}
};
export default config;
Application Code
3. Database Setup
// src/lib/database.js
import Database from 'better-sqlite3';
import { join } from 'path';
import { homedir } from 'os';
import { mkdirSync, existsSync } from 'fs';
// Create app data directory
const appDir = join(homedir(), '.my-notes-app');
if (!existsSync(appDir)) {
mkdirSync(appDir, { recursive: true });
}
// Initialize SQLite database
const dbPath = join(appDir, 'notes.db');
export const db = new Database(dbPath);
// Create tables
db.exec(`
CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Prepare statements for better performance
export const statements = {
getAllNotes: db.prepare('SELECT * FROM notes ORDER BY updated_at DESC'),
getNoteById: db.prepare('SELECT * FROM notes WHERE id = ?'),
insertNote: db.prepare('INSERT INTO notes (title, content) VALUES (?, ?)'),
updateNote: db.prepare(
'UPDATE notes SET title = ?, content = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?'
),
deleteNote: db.prepare('DELETE FROM notes WHERE id = ?')
};
4. API Routes
// src/routes/api/notes/+server.js
import { json } from '@sveltejs/kit';
import { statements } from '$lib/database.js';
export function GET() {
try {
const notes = statements.getAllNotes.all();
return json(notes);
} catch (error) {
console.error('Failed to fetch notes:', error);
return json({ error: 'Failed to fetch notes' }, { status: 500 });
}
}
export async function POST({ request }) {
try {
const { title, content } = await request.json();
if (!title || !content) {
return json({ error: 'Title and content are required' }, { status: 400 });
}
const result = statements.insertNote.run(title, content);
const note = statements.getNoteById.get(result.lastInsertRowid);
return json(note, { status: 201 });
} catch (error) {
console.error('Failed to create note:', error);
return json({ error: 'Failed to create note' }, { status: 500 });
}
}
// src/routes/api/notes/[id]/+server.js
import { json } from '@sveltejs/kit';
import { statements } from '$lib/database.js';
export function GET({ params }) {
try {
const note = statements.getNoteById.get(params.id);
if (!note) {
return json({ error: 'Note not found' }, { status: 404 });
}
return json(note);
} catch (error) {
console.error('Failed to fetch note:', error);
return json({ error: 'Failed to fetch note' }, { status: 500 });
}
}
export async function PUT({ params, request }) {
try {
const { title, content } = await request.json();
if (!title || !content) {
return json({ error: 'Title and content are required' }, { status: 400 });
}
const result = statements.updateNote.run(title, content, params.id);
if (result.changes === 0) {
return json({ error: 'Note not found' }, { status: 404 });
}
const note = statements.getNoteById.get(params.id);
return json(note);
} catch (error) {
console.error('Failed to update note:', error);
return json({ error: 'Failed to update note' }, { status: 500 });
}
}
export function DELETE({ params }) {
try {
const result = statements.deleteNote.run(params.id);
if (result.changes === 0) {
return json({ error: 'Note not found' }, { status: 404 });
}
return json({ success: true });
} catch (error) {
console.error('Failed to delete note:', error);
return json({ error: 'Failed to delete note' }, { status: 500 });
}
}
5. Frontend Components
<!-- src/lib/components/NoteCard.svelte -->
<script>
import { createEventDispatcher } from 'svelte';
export let note;
const dispatch = createEventDispatcher();
function handleEdit() {
dispatch('edit', note);
}
function handleDelete() {
if (confirm('Are you sure you want to delete this note?')) {
dispatch('delete', note.id);
}
}
function formatDate(dateString) {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
</script>
<div class="note-card">
<div class="note-header">
<h3>{note.title}</h3>
<div class="note-actions">
<button on:click={handleEdit} class="btn-edit">Edit</button>
<button on:click={handleDelete} class="btn-delete">Delete</button>
</div>
</div>
<div class="note-content">
{note.content}
</div>
<div class="note-meta">
<small>
Created: {formatDate(note.created_at)}
{#if note.updated_at !== note.created_at}
• Updated: {formatDate(note.updated_at)}
{/if}
</small>
</div>
</div>
<style>
.note-card {
border: 1px solid #e1e5e9;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
background: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.note-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.note-header h3 {
margin: 0;
color: #2d3748;
}
.note-actions {
display: flex;
gap: 8px;
}
.btn-edit,
.btn-delete {
padding: 4px 8px;
border: 1px solid;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.btn-edit {
border-color: #3182ce;
color: #3182ce;
background: white;
}
.btn-edit:hover {
background: #3182ce;
color: white;
}
.btn-delete {
border-color: #e53e3e;
color: #e53e3e;
background: white;
}
.btn-delete:hover {
background: #e53e3e;
color: white;
}
.note-content {
margin-bottom: 8px;
line-height: 1.5;
color: #4a5568;
white-space: pre-wrap;
}
.note-meta {
color: #718096;
border-top: 1px solid #e2e8f0;
padding-top: 8px;
}
</style>
<!-- src/lib/components/NoteForm.svelte -->
<script>
import { createEventDispatcher } from 'svelte';
export let note = { title: '', content: '' };
export let isEditing = false;
const dispatch = createEventDispatcher();
let title = note.title;
let content = note.content;
function handleSubmit() {
if (!title.trim() || !content.trim()) {
alert('Please fill in both title and content');
return;
}
dispatch('save', { title: title.trim(), content: content.trim() });
// Reset form if creating new note
if (!isEditing) {
title = '';
content = '';
}
}
function handleCancel() {
dispatch('cancel');
// Reset form
title = note.title;
content = note.content;
}
</script>
<form on:submit|preventDefault={handleSubmit} class="note-form">
<div class="form-group">
<label for="title">Title</label>
<input id="title" type="text" bind:value={title} placeholder="Enter note title..." required />
</div>
<div class="form-group">
<label for="content">Content</label>
<textarea
id="content"
bind:value={content}
placeholder="Write your note here..."
rows="6"
required
></textarea>
</div>
<div class="form-actions">
<button type="submit" class="btn-primary">
{isEditing ? 'Update Note' : 'Create Note'}
</button>
{#if isEditing}
<button type="button" on:click={handleCancel} class="btn-secondary"> Cancel </button>
{/if}
</div>
</form>
<style>
.note-form {
background: white;
border: 1px solid #e1e5e9;
border-radius: 8px;
padding: 16px;
margin-bottom: 24px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 4px;
font-weight: 500;
color: #2d3748;
}
.form-group input,
.form-group textarea {
width: 100%;
padding: 8px 12px;
border: 1px solid #e2e8f0;
border-radius: 4px;
font-family: inherit;
}
.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #3182ce;
box-shadow: 0 0 0 3px rgba(49, 130, 206, 0.1);
}
.form-actions {
display: flex;
gap: 8px;
}
.btn-primary,
.btn-secondary {
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
}
.btn-primary {
background: #3182ce;
color: white;
border: 1px solid #3182ce;
}
.btn-primary:hover {
background: #2c5aa0;
}
.btn-secondary {
background: white;
color: #4a5568;
border: 1px solid #e2e8f0;
}
.btn-secondary:hover {
background: #f7fafc;
}
</style>
6. Main Page
<!-- src/routes/+page.svelte -->
<script>
import { onMount } from 'svelte';
import NoteCard from '$lib/components/NoteCard.svelte';
import NoteForm from '$lib/components/NoteForm.svelte';
let notes = [];
let editingNote = null;
let loading = false;
onMount(() => {
loadNotes();
});
async function loadNotes() {
loading = true;
try {
const response = await fetch('/api/notes');
if (response.ok) {
notes = await response.json();
} else {
console.error('Failed to load notes');
}
} catch (error) {
console.error('Error loading notes:', error);
} finally {
loading = false;
}
}
async function createNote({ detail }) {
try {
const response = await fetch('/api/notes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(detail)
});
if (response.ok) {
await loadNotes(); // Refresh list
} else {
console.error('Failed to create note');
}
} catch (error) {
console.error('Error creating note:', error);
}
}
async function updateNote({ detail }) {
try {
const response = await fetch(`/api/notes/${editingNote.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(detail)
});
if (response.ok) {
editingNote = null;
await loadNotes(); // Refresh list
} else {
console.error('Failed to update note');
}
} catch (error) {
console.error('Error updating note:', error);
}
}
async function deleteNote({ detail: noteId }) {
try {
const response = await fetch(`/api/notes/${noteId}`, {
method: 'DELETE'
});
if (response.ok) {
await loadNotes(); // Refresh list
} else {
console.error('Failed to delete note');
}
} catch (error) {
console.error('Error deleting note:', error);
}
}
function startEditing({ detail: note }) {
editingNote = note;
}
function cancelEditing() {
editingNote = null;
}
</script>
<svelte:head>
<title>My Notes App</title>
<meta name="description" content="A simple note-taking app built with SvelteKit" />
</svelte:head>
<main>
<div class="container">
<header>
<h1>My Notes</h1>
<p>A simple note-taking application</p>
</header>
{#if editingNote}
<section>
<h2>Edit Note</h2>
<NoteForm
note={editingNote}
isEditing={true}
on:save={updateNote}
on:cancel={cancelEditing}
/>
</section>
{:else}
<section>
<h2>Create New Note</h2>
<NoteForm on:save={createNote} />
</section>
{/if}
<section>
<h2>Your Notes</h2>
{#if loading}
<p>Loading notes...</p>
{:else if notes.length === 0}
<p>No notes yet. Create your first note above!</p>
{:else}
{#each notes as note (note.id)}
<NoteCard {note} on:edit={startEditing} on:delete={deleteNote} />
{/each}
{/if}
</section>
</div>
</main>
<style>
:global(body) {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f7fafc;
line-height: 1.6;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
margin-bottom: 32px;
padding: 24px;
background: white;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
header h1 {
margin: 0;
color: #2d3748;
font-size: 2.5rem;
}
header p {
margin: 8px 0 0 0;
color: #718096;
font-size: 1.1rem;
}
section {
margin-bottom: 32px;
}
section h2 {
margin-bottom: 16px;
color: #2d3748;
font-size: 1.5rem;
}
</style>
Building and Running
7. Install Dependencies
# Install required dependencies
npm install better-sqlite3
8. Package Configuration
{
"name": "my-notes-app",
"version": "1.0.0",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"start": "./build/my-notes-app"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"sveltekit-exec-adapter": "^1.0.0",
"svelte": "^4.2.7",
"vite": "^5.0.3"
},
"dependencies": {
"better-sqlite3": "^9.0.0"
}
}
9. Build the Executable
# Build the application
npm run build
# The executable will be created at: build/my-notes-app
10. Test the Application
# Run the executable
./build/my-notes-app
# Or on Windows
./build/my-notes-app.exe
The application will start on http://localhost:3000 and you can:
- Create new notes
- Edit existing notes
- Delete notes
- All data persists locally in SQLite
What You’ve Learned
This example demonstrates:
- Database Integration: Using SQLite for local data persistence
- API Routes: Creating RESTful endpoints for CRUD operations
- Component Architecture: Building reusable Svelte components
- Error Handling: Proper error handling in both frontend and backend
- Static Assets: Embedding CSS and other static files
- File System: Storing application data in user’s home directory
The resulting executable is completely self-contained and requires no external dependencies to run!