Build a Full-Stack SvelteKit App with Server Actions and Drizzle ORM
Introduction
This guide demonstrates how to build a basic full-stack application using SvelteKit, Server Actions, and Drizzle ORM. We'll cover setting up your environment, defining your schema, and implementing basic CRUD operations.
Prerequisites
- Node.js (version 18 or higher)
- npm or yarn
- Basic knowledge of SvelteKit and JavaScript
Setup
-
Create a new SvelteKit project:
npm create svelte@latest my-sveltekit-app cd my-sveltekit-app npm install
-
Install Drizzle ORM and necessary dependencies:
npm install drizzle-orm @neondatabase/serverless pg pg-format dotenv npm install -D drizzle-kit
-
Create a
.env
file:DATABASE_URL="your_neon_database_url" # e.g., postgres://user:password@host:port/database
Define Your Schema
Create a src/lib/server/db/schema.ts
file:
// src/lib/server/db/schema.ts
import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content'),
createdAt: timestamp('created_at').defaultNow(),
});
export type Post = typeof posts.$inferSelect; // return type when selecting data
export type InsertPost = typeof posts.$inferInsert; // insert type
Connect to the Database
Create a src/lib/server/db/index.ts
file:
// src/lib/server/db/index.ts
import { drizzle } from 'drizzle-orm/node-postgres';
import { Client } from 'pg';
import * as schema from './schema';
import { DATABASE_URL } from '$env/static/private'; // Important for production
const client = new Client({
connectionString: DATABASE_URL,
});
client.connect();
export const db = drizzle(client, { schema });
Generate Migrations
-
Configure Drizzle Kit: Create a
drizzle.config.ts
file at the root of your project.// drizzle.config.ts import type { Config } from "drizzle-kit"; import { loadEnvConfig } from "@next/env"; loadEnvConfig({ projectDir: "." }); export default { schema: "./src/lib/server/db/schema.ts", out: "./drizzle", driver: "pg", dbCredentials: { connectionString: process.env.DATABASE_URL || "", }, verbose: true, strict: true, } satisfies Config;
-
Generate and run migrations:
npx drizzle-kit generate:pg npx drizzle-kit push:pg
Implement Server Actions
Create a file src/lib/actions.ts
:
// src/lib/actions.ts
'use server';
import { db } from './server/db';
import { posts } from './server/db/schema';
import { revalidatePath } from 'next/cache';
import {redirect} from 'sveltekit-flash-message/server';
import { message, superValidate } from 'sveltekit-flash-message/server';
import {postSchema} from './schemas'
import { fail } from '@sveltejs/kit';
export async function createPost(formData: FormData) {
const schema = await superValidate(formData, postSchema);
if (!schema.valid) {
return fail(400, { schema })
}
const {title, content} = schema.data
try {
await db.insert(posts).values({ title: title, content: content });
} catch (e: any) {
console.error(e);
return message(schema, 'Failed to create post', { status: 500 });
}
revalidatePath('/');
throw redirect(303, '/', { type: 'success', message: 'Post created successfully!' });
}
Create the Form and Display Posts
Modify src/routes/+page.svelte
:
<!-- src/routes/+page.svelte -->
<script>
import { createPost } from '$lib/actions';
import { enhance } from '$app/forms';
import { superForm } from 'sveltekit-flash-message/client';
import {postSchema} from '$lib/schemas'
import { invalidateAll } from '$app/navigation';
import type { PageData } from './$types';
import { browser } from '$app/environment';
export let data: PageData;
$: ({ posts } = data)
const { form, enhance: enhancedForm, errors, message } = superForm(postSchema, {
onSubmit: async ({ result, submitter }) => {
if (result.type === 'success') {
if (browser) {
invalidateAll();
}
}
}
});
</script>
{#if $message}
<div class="alert alert-success">{$message.message}</div>
{/if}
<form method="POST" action="?/createPost" use:enhancedForm>
<label for="title">Title:</label>
<input type="text" id="title" name="title" bind:value={$form.title} aria-invalid={$errors.title ? 'true' : undefined}>
{#if $errors.title}
<div class="error">{$errors.title}</div>
{/if}
<label for="content">Content:</label>
<textarea id="content" name="content" bind:value={$form.content}></textarea>
<button type="submit">Create Post</button>
{#if $message && $message.message && $message.status === 500}
<div class="error">{$message.message}</div>
{/if}
</form>
<h2>Posts</h2>
{#each posts as post}
<div>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
{/each}
Modify src/routes/+page.server.ts
to load the initial posts:
// src/routes/+page.server.ts
import { db } from '$lib/server/db';
import { posts } from '$lib/server/db/schema';
import type { PageServerLoad } from './$types';
import {createPost} from '$lib/actions'
export const load: PageServerLoad = async () => {
const allPosts = await db.select().from(posts);
return {
posts: allPosts,
};
};
export const actions = {createPost}
Finally, install sveltekit-flash-message
and @sveltejs/adapter-auto
for the flash messages. Add import { dev } from '$app/environment';
in src/hooks.server.js
and +layout.ts
and use a sequence
if needed as described in the sveltekit-flash-message
documentation to set up flash messages, and add the adapter into svelte.config.js
.
Also create schemas src/lib/schemas.ts
:
import { z } from 'zod';
export const postSchema = z.object({
title: z.string().min(3).max(100),
content: z.string().min(10)
});
Conclusion
This guide provides a basic foundation for building full-stack SvelteKit applications with Server Actions and Drizzle ORM. You can expand upon this foundation by adding authentication, more complex data models, and advanced UI features. Remember to always secure your database credentials and properly validate user input in production.