Back to articles

Build a Full-Stack SvelteKit App with Server Actions and Drizzle ORM

AuthorMajd Muhtaseb04/30/202515 minutes
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

  1. Create a new SvelteKit project:

    npm create svelte@latest my-sveltekit-app
    cd my-sveltekit-app
    npm install
    
  2. Install Drizzle ORM and necessary dependencies:

    npm install drizzle-orm @neondatabase/serverless pg pg-format dotenv
    npm install -D drizzle-kit
    
  3. 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

  1. 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;
    
  2. 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.