No description
  • TypeScript 52.3%
  • Svelte 46%
  • JavaScript 1.2%
  • HTML 0.5%
Find a file
2025-10-13 22:18:09 +01:00
assets week5 lab 2025-10-13 22:18:09 +01:00
e2e week5 lab 2025-10-11 23:06:27 +01:00
src week5 lab 2025-10-11 23:06:27 +01:00
static week5 lab 2025-10-11 23:06:27 +01:00
.env.example week5 lab 2025-10-11 23:06:27 +01:00
.gitignore week5 lab 2025-10-11 23:06:27 +01:00
.npmrc week5 lab 2025-10-11 23:06:27 +01:00
.prettierignore week5 lab 2025-10-11 23:06:27 +01:00
.prettierrc week5 lab 2025-10-11 23:06:27 +01:00
drizzle.config.ts week5 lab 2025-10-11 23:06:27 +01:00
package-lock.json week5 lab 2025-10-11 23:06:27 +01:00
package.json week5 lab 2025-10-11 23:06:27 +01:00
playwright.config.ts week5 lab 2025-10-11 23:06:27 +01:00
README.md week5 lab 2025-10-13 22:18:09 +01:00
README_old.md week5 lab 2025-10-13 22:18:09 +01:00
saple_data.sql week5 lab 2025-10-11 23:06:27 +01:00
svelte.config.js week5 lab 2025-10-11 23:06:27 +01:00
tsconfig.json week5 lab 2025-10-11 23:06:27 +01:00
vite.config.ts week5 lab 2025-10-11 23:06:27 +01:00
vitest-setup-client.ts week5 lab 2025-10-11 23:06:27 +01:00

Lab 5: SvelteKit Components + Data Access

Enda Lee 2025

PDF version

Getting Started

  1. Clone or download this repository.
  2. Open the app folder in Visual Studio Code.
  3. Create a .env file in the application root using .env.example as a template.
  4. In the terminal, run npm install to install dependencies.
  5. Start the development server with npm run dev.

Overview

This lab demonstrates how to build a fully functional form workflow in SvelteKit with:

  • Type-safe validation using Zod schemas generated from Drizzle ORM tables.
  • Progressive form handling with SvelteKit form actions.
  • Accessibility support that meets WCAG 2.1 AA standards.
  • A structured, layered architecture for maintainable code.
  • Unified validation and error reporting on both server and client.

1. Application Architecture

Designing a web application for long-term maintainability and security requires clear separation of responsibilities between the client and server.

A layered architecture supports this by dividing responsibilities:

Layer Responsibility
UI Layer Components, forms, and layouts
Server Actions Form submission and request handling
Service Layer Validation, authorization, and business rules
Data Access Layer (DAL) Database operations through Drizzle ORM
Database Layer Table definitions, constraints, and relations

This separation ensures each layer performs a single function, making the application more secure, maintainable, and testable.

UI Components  →  Server Actions  →  Services  →  Data Access  →  Database

architecture-diagram

📁 LAB5-START
└── 📁 src
    ├── 📁 lib
    │   ├── 📁 assets
    │   ├── 📁 components
    │   │   ├── CategoryButton.svelte
    │   │   ├── LocationCard.svelte
    │   │   └── LocationForm.svelte
    │   └── 📁 server
    │       ├── 📁 data-access
    │       ├── 📁 db
    │       ├── 📁 services
    │       ├── 📁 utils
    │       ├── 📁 types
    │       └── index.ts
    ├── 📁 routes
    │   ├── 📁 location
    │   │   ├── +page.server.ts
    │   │   └── +page.svelte
    │   └── Other routes...

This structure aligns with separation of concerns principles used in enterprise applications.

2. Database Schema & Validation

The database schema from the previous lab is reused here. It includes category, user, and location tables with one-to-many relationships between them.

Each location record references a category and a user.

2.1 Zod-Based Validation

Zod provides a type-safe, declarative way to validate user input. Drizzle ORM integrates tightly with Zod, allowing schemas to be inferred directly from table definitions.

Install dependencies
npm install drizzle-zod zod

Define Zod validation schemas

In src/lib/server/db/validation.ts, define validation rules for each model. Each field should include type requirements and customized error messages.

example:

import { z } from 'zod';

export const insertLocationSchema = z.object({
  name: z.string().min(1, 'Name is required'),
  description: z.string().min(1, 'Description is required'),
  latitude: z.number().min(-90).max(90),
  longitude: z.number().min(-180).max(180),
});

Infer TypeScript Types

Zod can infer static types automatically, keeping code consistent:

export type LocationInput = z.infer<typeof insertLocationSchema>;
Benefits of Schema-Based Validation
  • Single source of truth: The schema defines both validation and typing.
  • Type safety: Prevents runtime type errors.
  • Reduced redundancy: Eliminates duplicate validation logic.
  • Better tooling: IDE autocompletion and static analysis.

3. Data Access Layer

The Data Access Layer centralizes all database operations, providing a clean interface for queries and updates. Keep your DAL functions pure — they should only handle data interactions, not business logic.

Example: src/lib/server/data-access/locations.ts

data access functions example

Why Use a DAL?

  • Prevents code duplication across pages or services.
  • Makes database changes easier (e.g., replacing SQLite with PostgreSQL).
  • Improves maintainability and security.

4. Business Logic Layer (services)

The Service Layer performs validation, permission checks, and applies domain rules before database operations occur. This layer uses both Zod validators and DAL functions.

Example: src/lib/server/services/location-service.ts

location service

5. UI Layer

The UI Layer renders data, handles user input, and manages state. This lab focuses on the location route.

locations display

5.1. Loading Data (+page.server.ts)

Rather than directly accessing the database, load data by calling service-layer functions. A temporary value like currentUserId = 1 can be used during local development.

5.2. Page Component (+page.svelte)

In Svelte 5, the new runes syntax replaces export let data;.

Example:

<script lang="ts">
  let { data } = $props();
  let categories = $state<Category[]>(data.categories);
  let locations = $state<Location[]>(data.locations);
</script>

This makes reactive variables explicit and strongly typed.

5.3. Reusable Components

CategoryButton Component

Each category is rendered using a shared <CategoryButton> component.

<CategoryButton
  category={cat}
  isSelected={selectedCategoryId === cat.id}
  oncategoryClick={handleCategoryClick}
/>

This promotes reuse and ensures consistent UI behavior.

LocationCard Component

The <LocationCard> component renders a location and an interactive Leaflet map using Sveaflet.

Install dependencies:

npm install sveaflet leaflet @types/leaflet

Maps automatically update when data changes.

5.1. Loading the data - +page.server.js

Instead of querying the database directly, this file imports and calls service functions (for example, getAllLocations).

A placeholder currentUserId is used for now.

Load location data for he page

6. Adding new Locations

6.1. Form Handling Using Actions

SvelteKit’s form actions support both JavaScript-enhanced and non-JavaScript environments.

Example form:
<form
  method="POST"
  action="?/createLocation"
  use:enhance={({ result }) => {
    if (result.type === 'success') message = 'Location created!';
    else if (result.type === 'failure') errors = result.data.errors;
  }}
>
  <label for="name">Name</label>
  <input id="name" name="name" required aria-required="true" />
  {#if errors.name}<div class="error">{errors.name}</div>{/if}
  <button disabled={isSubmitting}>Save</button>
</form>
Corresponding Server-side action:
export const actions = {
  createLocation: async ({ request }) => {
    const formData = await request.formData();
    try {
      await locationService.createLocation(/* extracted data */);
      return { success: true };
    } catch (err) {
      return fail(400, { errors: { name: 'Invalid name' } });
    }
  },
};

6.2 Accessibility Requirements

Follow WCAG 2.1 AA standards:

  • Always connect <label> and <input> using for and id.
  • Use aria-required="true" and aria-invalid="true" for validation feedback.
  • Use aria-describedby to link fields to error messages.
  • Maintain visible keyboard focus states.
Example:
<input
  id="name"
  name="name"
  aria-invalid={errors.name ? 'true' : 'false'}
  aria-describedby={errors.name ? 'name-error' : undefined}
/>
{#if errors.name}
  <div id="name-error" class="invalid-feedback">{errors.name}</div>
{/if}

This ensures accessibility for screen reader users.

7. The LocationForm Component

The reusable <LocationForm> component supports both create and edit modes.

Script Section
<script lang="ts">
  import { enhance, applyAction } from '$app/forms';
  import { invalidateAll } from '$app/navigation';
  import type { Category, Location } from '$lib/types';

  interface Props {
    categories: Category[];
    location?: Location | null;
    userId: number;
    oncancel?: () => void;
  }

  let { categories, location = null, userId, oncancel }: Props = $props();
  let isEditMode = $derived(location !== null);
  let formTitle = $derived(isEditMode ? 'Edit Location' : 'Add New Location');
</script>

HTML Section Highlights

  • All fields are bound to reactive state variables.
  • Error messages and ARIA attributes meet WCAG 2.1 AA accessibility standards.
  • Use Bootstrap classes for layout and styling.

The form includes: name, description, category, latitude, longitude, plus shared and favourite checkboxes.

On successful submission:

  1. The form shows a success message.
  2. The parent page is refreshed using invalidateAll().
  3. The form resets automatically.

Exercises

  1. Test full create, update, and validation functionality.
  2. Identify two issues or unexpected behaviors.
  3. Rebuild the Products page using database data instead of dummyjson.com.
  4. Reuse the existing CategoryButton component.
  5. Create a ProductForm component, based on LocationForm.

Enda Lee 2025