Building Modern Web Apps with SvelteKit: From Setup to Production Deployment

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

Why SvelteKit Became My Go-To Framework

A few years back I was maintaining a mid-size React app for a client. Bundle size kept creeping up, hydration errors appeared randomly, and every new developer on the team needed a week just to understand the state management setup. A colleague suggested I try SvelteKit for the next project. I was skeptical — another JavaScript framework? But after one weekend of experimenting, I never looked back.

SvelteKit compiles your components to vanilla JavaScript at build time. There’s no virtual DOM, no runtime framework overhead, and the mental model is refreshingly simple. You write .svelte files that look like enhanced HTML, and the compiler handles the rest. For server-side rendering, static generation, and API routes, everything lives in one project with a file-based routing system that just makes sense.

Three months later, the new project had shipped. Build output sat under 80KB for the entry bundle — compared to 340KB for the React app it replaced — and new team members were productive within a day, not a week. SvelteKit covers the full stack without forcing you into a complex ecosystem from day one.

Installation and Project Setup

You’ll need Node.js 18 or higher. Check your version first:

node --version
npm --version

Scaffold a new SvelteKit project using the official CLI:

npm create svelte@latest my-sveltekit-app
cd my-sveltekit-app
npm install

The CLI asks a few questions. For a real project I typically choose:

  • Template: Skeleton project (gives you a clean slate)
  • Type checking: TypeScript (worth the setup cost on any project you’ll maintain)
  • Prettier + ESLint: Yes — saves arguments in code review

Start the dev server:

npm run dev

Open http://localhost:5173 and you’ll see the default page. Save a file and changes appear instantly — the dev server uses Vite’s hot module replacement, so application state isn’t reset between edits.

Project Structure You Need to Understand

File-based routing is central to how SvelteKit works. The src/routes directory maps directly to URL paths:

src/
├── routes/
│   ├── +page.svelte          ← renders at /
│   ├── about/
│   │   └── +page.svelte      ← renders at /about
│   ├── blog/
│   │   ├── +page.svelte      ← renders at /blog
│   │   └── [slug]/
│   │       └── +page.svelte  ← renders at /blog/any-slug
│   └── api/
│       └── posts/
│           └── +server.ts    ← API endpoint at /api/posts
├── lib/
│   └── components/           ← reusable components
└── app.html                  ← base HTML template

That + prefix on filenames is intentional — it distinguishes SvelteKit special files from your own files in the same directory.

Configuration: SSR, Adapters, and Environment Variables

The svelte.config.js file is the heart of your SvelteKit configuration. By default it ships with the auto adapter, which works for most Node.js environments:

import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter()
  }
};

export default config;

For production deployments you’ll want a specific adapter. The most common ones:

  • @sveltejs/adapter-node — Node.js server (VPS, Docker)
  • @sveltejs/adapter-vercel — Vercel Edge/Serverless
  • @sveltejs/adapter-static — fully static output (Nginx, CDN)

Switch to the Node adapter for a VPS deployment:

npm install -D @sveltejs/adapter-node
import adapter from '@sveltejs/adapter-node';

const config = {
  kit: {
    adapter: adapter({
      out: 'build'  // output directory
    })
  }
};

Handling Environment Variables

One sharp design choice: SvelteKit enforces a hard split between server secrets and public variables. In a .env file:

# Public — exposed to browser
PUBLIC_API_URL=https://api.example.com

# Private — server-side only
DATABASE_URL=postgresql://user:pass@localhost/mydb
SECRET_KEY=your-secret-here

Access them in code:

// In +page.server.ts or +server.ts (server-side only)
import { DATABASE_URL } from '$env/static/private';

// In +page.svelte (client-safe)
import { PUBLIC_API_URL } from '$env/static/public';

SvelteKit throws a build error if you accidentally import a private variable into client-side code. That guardrail has saved me from leaking API keys more than once.

Building Your First Page with Data Loading

One of SvelteKit’s cleanest patterns is the load function. Create a blog listing page:

// src/routes/blog/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ fetch }) => {
  const response = await fetch('/api/posts');
  const posts = await response.json();
  return { posts };
};
<!-- src/routes/blog/+page.svelte -->
<script lang="ts">
  import type { PageData } from './$types';
  export let data: PageData;
</script>

<h1>Blog</h1>
{#each data.posts as post}
  <article>
    <h2><a href="/blog/{post.slug}">{post.title}</a></h2>
    <p>{post.excerpt}</p>
  </article>
{/each}

Data flows from server to component with full TypeScript types generated automatically. No Redux, no context API wiring, no prop drilling through three layers of components.

Building and Deploying to Production

Build the production bundle:

npm run build

# Preview the production build locally
npm run preview

With the Node adapter, the build/ directory contains a standalone Node.js server. Deploy it with:

# On your server
node build/index.js

Or create a simple Dockerfile:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY build ./build
EXPOSE 3000
CMD ["node", "build/index.js"]
docker build -t my-sveltekit-app .
docker run -p 3000:3000 -e PORT=3000 my-sveltekit-app

Set the PORT environment variable to control which port the server binds to. Behind Nginx, add a reverse proxy block:

server {
  listen 80;
  server_name yourdomain.com;

  location / {
    proxy_pass http://localhost:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection 'upgrade';
    proxy_set_header Host $host;
    proxy_cache_bypass $http_upgrade;
  }
}

Verification and Monitoring

Once deployed, verify SSR is working. Request the page with curl and check that HTML content arrives pre-rendered:

curl -s https://yourdomain.com/blog | grep -o '<h1>.*</h1>'

If you see the heading in the raw HTML response — not empty <div> containers — SSR is working. A blank body means the page is rendering client-side only. Check your load functions and adapter configuration.

Performance Baseline

Run a quick Lighthouse check from the command line:

npx lighthouse https://yourdomain.com --output json --quiet | \
  node -e "const d=require('fs').readFileSync('/dev/stdin','utf8'); \
           const r=JSON.parse(d); \
           console.log('Performance:', r.categories.performance.score * 100);"

Scores of 90–98 on performance are common out of the box. There’s no runtime framework to download — just compiled component code plus whatever you import yourself.

Logging and Error Tracking

Error hooks live in src/hooks.server.ts — built in, no extra packages needed:

import type { HandleServerError } from '@sveltejs/kit';

export const handleError: HandleServerError = ({ error, event }) => {
  console.error('Server error:', error, 'on path:', event.url.pathname);
  // Send to Sentry, Datadog, etc.
  return {
    message: 'Something went wrong. We\'ve been notified.'
  };
};

For production monitoring, point PM2 at the build output:

npm install -g pm2
pm2 start build/index.js --name sveltekit-app -i max
pm2 save
pm2 startup

The -i max flag runs one instance per CPU core — horizontal scaling without changing a line of application code.

After two years of shipping SvelteKit projects, what I keep coming back to is how little there is to fight. Small API surface, excellent docs, and a Vite-powered dev server that stays fast as the project grows. If your current framework has started to feel like overhead rather than tooling, give this a proper weekend. The comparison makes itself.

Share: