Skip to content
ExponaBLOG
SVELTE & SVELTEKIT · DAY 100 / 100

100 days of Svelte and SvelteKit: the recap in practice

The lessons that matter most.

Expona Admin7 min read

100 days of Svelte and SvelteKit: the recap in practice

We've spent 100 days building real applications with Svelte and SvelteKit, and the patterns that stuck with us aren't the flashy ones—they're the ones that make our codebases faster to ship, easier to maintain, and genuinely pleasant to work in. This recap focuses on the lessons that matter most in production, the ones we reach for automatically now when we start a new project or refactor existing code.

Runes simplify reactivity — $state, $derived and careful $effect

Svelte 5 runes are not just syntactic sugar. They're a fundamental shift in how we think about reactive dependencies. The old $-prefixed assignment syntax was clever but implicit; runes make the flow of data intentional and verifiable.

$state is straightforward—it marks a variable as reactive. But the real power comes from using it alongside $derived to build computed values that automatically update without us manually tracking dependencies:

typescript
let count = $state(0);
let doubled = $derived(count * 2);
let isEven = $derived(count % 2 === 0);

function increment() {
  count++;
  // doubled and isEven update automatically, no manual subscriptions
}

In a real e-commerce context, consider a shopping cart. We track items, and need to compute totals, tax, and discount eligibility:

typescript
let cartItems = $state([
  { id: 1, price: 50, quantity: 2 },
  { id: 2, price: 30, quantity: 1 }
]);

let subtotal = $derived(
  cartItems.reduce((sum, item) => sum + (item.price * item.quantity), 0)
);

let tax = $derived(subtotal * 0.1);
let qualifiesForFreeShip = $derived(subtotal >= 100);
let total = $derived(subtotal + tax);

$effect is where we must be careful. It runs side effects, but we should only reach for it when computed values aren't enough. A common mistake is using $effect to update state directly—that's what $derived is for. Use $effect only for truly external side effects: API calls, analytics, DOM focus, subscriptions.

typescript
// Good: side effect only
$effect(() => {
  console.log(`Cart total is now $${total}`);
  analytics.track('cart_updated', { total });
});

// Avoid: deriving state in an effect
$effect(() => {
  let result = cartItems.filter(item => item.quantity > 0);
  itemCount = result.length; // Use $derived instead
});

The discipline here is: if the value depends only on other state, use $derived. If it touches the outside world, use $effect.

SvelteKit is full-stack — Loads, actions and hooks

SvelteKit blurs the line between frontend and backend in a way that makes building full-stack features feel natural. We're not just building an SPA that happens to call an API—we're building a cohesive application where the server and client are two parts of the same system.

Loads run on the server before rendering. This means authentication checks, database queries, and sensitive data fetching all happen server-side. The data flows to the page as props:

typescript
// src/routes/orders/+page.server.ts
import { redirect } from '@sveltejs/kit';

export async function load({ locals }) {
  if (!locals.user) {
    throw redirect(303, '/login');
  }

  const orders = await db.orders.findMany({
    where: { userId: locals.user.id }
  });

  return { orders };
}

In a real ride-hailing platform, this load function might fetch active rides, driver location history, and pricing tiers—all secure, all pre-rendered into the initial HTML.

Actions are server-side handlers for form submissions. Instead of writing fetch boilerplate, we define named actions and invoke them from the client:

typescript
// src/routes/driver/+page.server.ts
export const actions = {
  updateStatus: async ({ request, locals }) => {
    const formData = await request.formData();
    const status = formData.get('status');

    await db.drivers.update({
      where: { id: locals.user.id },
      data: { status }
    });

    return { success: true };
  }
};

Then in the component:

typescript
// src/routes/driver/+page.svelte



  
    Go Online
  

Hooks are middleware that run on every request. Common uses include setting up session data, logging, rate limiting, or modifying responses:

typescript
// src/hooks.server.ts
export async function handle({ event, resolve }) {
  const token = event.cookies.get('auth_token');
  
  if (token) {
    event.locals.user = await verifyToken(token);
  }

  return resolve(event);
}

The elegance here is that we're not juggling API routes and frontend code in separate mental models. The server work is visible, the data flow is explicit, and security concerns are built in from the start.

Progressive enhancement isn't a nice-to-have—it's the foundation of resilient web applications. In SvelteKit, it's nearly free. A form with method="POST" works without JavaScript. Add use:enhance and it becomes a smooth, client-side experience that doesn't break if JavaScript fails to load or execute.

Consider a CRM system where users need to update customer contact details. Without progressive enhancement, any JavaScript bundle delay or network glitch leaves users unable to save:

typescript
// src/routes/customers/[id]/+page.svelte


 {
  submitResult = result;
}}>
  
  
  Save

This form works immediately on page load, works without JavaScript, and provides optimistic feedback when JavaScript is available. The same principle applies to navigation links—use native <a> elements for navigation and let SvelteKit handle preloading and soft transitions.

We've seen production issues where teams reach for JavaScript-only navigation, then discover search engines struggle to crawl, old browsers break, and network latency multiplies perceived load times. Progressive enhancement side-steps all of it.

Ship fast pages — Prerendering, SEO and adapters

Page speed is a ranking factor, a conversion factor, and a user experience factor. SvelteKit gives us multiple levers to pull.

Prerendering generates static HTML at build time for routes that don't need server-side rendering. A product listing page, blog posts, or static content pages become instant on the client:

typescript
// src/routes/products/+page.ts
export const prerender = true;

For an e-commerce site with hundreds of SKUs, prerendering product detail pages cuts server load and time-to-interactive dramatically. The HTML is already built when the browser requests it.

SEO metadata lives in +page.svelte via the <svelte:head> block or exported from load functions:

typescript
// src/routes/products/[id]/+page.ts
export async function load({ params }) {
  const product = await getProduct(params.id);
  
  return {
    product,
    title: product.name,
    description: product.shortDescription
  };
}

Then in the component:

typescript
// src/routes/products/[id]/+page.svelte



  {data.title}
  
  

For adapters, the choice depends on deployment. @sveltejs/adapter-node gives us control for platforms like AWS ECS or dedicated servers. @sveltejs/adapter-vercel or @sveltejs/adapter-netlify optimize for those platforms' edge functions and cold-start constraints. @sveltejs/adapter-static for fully prerendered sites that deploy as flat files.

We've shipped SvelteKit applications across all three patterns, and the adapter abstraction is solid—the core application code doesn't change.

Common mistakes

After 100 days, we see patterns in what trips teams up:

  • Overusing $effect — Using it to derive state instead of using $derived. Every effect is a potential source of infinite loops or stale closures. Lean on $derived first.
  • Fetching in routes — Using +page.ts instead of +page.server.ts for data that must be secure. Client loads expose your queries in network tabs. Keep sensitive queries server-side.
  • Skipping progressive enhancement — Adding JavaScript event handlers to forms instead of using native form submission with actions. This trades robustness for perceived control.
  • Not prerendering static content — Leaving prerender off for pages that don't change frequently. Every request hits the server unnecessarily.
  • Ignoring load dependencies — Forgetting that +page.server.ts loads depend on +layout.server.ts loads. Bugs hide in unexpected data inheritance chains.

Key takeaways

  • Use $state for reactive values, $derived for computed values, $effect only for side effects with external systems.
  • Loads, actions, and hooks form a complete server-side framework—leverage them to keep logic centralized and secure.
  • Progressive enhancement is not retro—it's the fastest path to resilient, crawlable applications.
  • Prerender static routes, use the right adapter for your deployment, and bake SEO metadata into page loads for automatic indexing.
  • SvelteKit's architecture is built around the request-response cycle, not around trying to hide the server. Embrace that model and much of the complexity dissolves.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles