10x Your SvelteKit Code: My 2025 Vite Decorator Plugin
Discover how a new Vite decorator plugin for SvelteKit can 10x your productivity. Clean up boilerplate, streamline validation, and write elegant code in 2025.
Alexandre Dubois
Senior Full-Stack Engineer specializing in SvelteKit, performance optimization, and developer tooling.
Introduction: The Silent Struggle with Boilerplate
SvelteKit is a dream to work with. Its file-based routing, server-side rendering, and seamless data loading functions make building robust web applications faster than ever. But as our applications grow, a familiar enemy creeps in: boilerplate. We find ourselves writing the same checks, the same validation logic, and the same response headers in our `+page.server.ts` and `+server.ts` files, over and over again.
What if we could abstract away this repetitive code? What if we could declare our intentions—caching, validation, authorization—with a single line? Today, I'm thrilled to introduce a solution that does just that. This is the story of how I built a Vite decorator plugin for SvelteKit that will change the way you write server-side code, making it cleaner, more readable, and up to 10x more efficient.
What Are Decorators (and Why You Should Care)?
For those coming from backgrounds like Angular, NestJS, or even Python, decorators are a familiar concept. For others, they might seem like magic. In essence, decorators are special kinds of declarations that can be attached to a class, method, accessor, property, or parameter. They are functions that wrap another function or class to modify its behavior.
Think of them as wrappers. You have a core function, and you wrap it with a decorator to add new functionality—like logging, timing, or access control—without cluttering the core function's logic. With the ECMAScript Decorators proposal stabilizing, their use in the JavaScript ecosystem is set to explode. We're bringing that power to SvelteKit, today.
The Problem: Repetitive Logic in SvelteKit
Let's look at a typical SvelteKit form action. It needs to check if the user is authenticated, validate the incoming `FormData` against a schema, and then perform its core logic. The code often looks something like this:
// src/routes/profile/+page.server.ts
import { z } from 'zod';
import { fail, redirect } from '@sveltejs/kit';
const profileSchema = z.object({
name: z.string().min(2),
bio: z.string().optional(),
});
export const actions = {
updateProfile: async ({ request, locals }) => {
// 1. Authentication check
if (!locals.user) {
throw redirect(303, '/login');
}
const formData = await request.formData();
const data = Object.fromEntries(formData);
// 2. Validation logic
const validationResult = profileSchema.safeParse(data);
if (!validationResult.success) {
return fail(400, { issues: validationResult.error.issues });
}
// 3. Core logic
await db.updateUser(locals.user.id, validationResult.data);
return { success: true };
},
};
This isn't terrible, but imagine having dozens of these actions. The auth check and validation block appear everywhere. It's verbose, error-prone, and obscures the primary purpose of the function: updating the user profile.
The Solution: Introducing `vite-sveltekit-decorators`
This is where my new plugin, `vite-sveltekit-decorators`, comes in. It's a Vite plugin that enables a suite of powerful, SvelteKit-aware decorators for your server-side code. It lets you refactor the above mess into something beautifully declarative:
// src/routes/profile/+page.server.ts (with decorators)
import { z } from 'zod';
import { Guard, Validate } from 'vite-sveltekit-decorators';
const profileSchema = z.object({
name: z.string().min(2),
bio: z.string().optional(),
});
export const actions = {
// Look how clean this is!
@Guard((locals) => !!locals.user, { redirect: '/login' })
@Validate(profileSchema)
updateProfile: async ({ locals, validatedData }) => {
// Auth and validation are handled automatically!
// The validated data is injected for you.
await db.updateUser(locals.user.id, validatedData);
return { success: true };
},
};
All the boilerplate is gone. The intent of the function is crystal clear. The `@Guard` decorator handles authentication, and the `@Validate` decorator handles parsing and validation, automatically returning a `fail(400)` on error or providing the clean `validatedData` on success.
Core Features & Examples
The plugin ships with a set of essential decorators designed to tackle the most common SvelteKit server-side tasks.
`@Cache`: Declarative Server-Side Caching
Tired of manually setting `Cache-Control` headers in your `load` functions? Use the `@Cache` decorator to define caching strategies declaratively.
// src/routes/blog/[slug]/+page.server.ts
import { Cache } from 'vite-sveltekit-decorators';
export class PageServer {
// Cache this response for 5 minutes on the CDN, 1 minute in the browser
@Cache({ 's-maxage': 300, 'max-age': 60 })
async load({ params }) {
const post = await db.getPost(params.slug);
return { post };
}
}
The decorator automatically calls `event.setHeaders()` with the provided object, keeping your load function focused on loading data.
`@Validate`: Effortless Schema Validation
As shown before, `@Validate` is a game-changer for form actions and API endpoints. It integrates seamlessly with Zod (and other validation libraries) to parse and validate `request.formData()` or `request.json()`.
// src/routes/api/contact/+server.ts
import { z } from 'zod';
import { json } from '@sveltejs/kit';
import { Validate } from 'vite-sveltekit-decorators';
const contactSchema = z.object({
email: z.string().email(),
message: z.string().min(10)
});
// Works for API routes too!
@Validate(contactSchema, { from: 'json' })
export async function POST({ validatedData }) {
// No need for request.json() or try/catch blocks
await sendContactEmail(validatedData);
return json({ message: 'Success' });
}
`@Guard`: Robust Route Protection
The `@Guard` decorator is your new best friend for authorization. It runs a check function you provide. If it returns `false`, the guard will either throw a `403 Forbidden` error or perform a redirect, halting the execution of your function.
// Only admins can access this action
const isAdmin = (locals) => locals.user?.role === 'ADMIN';
export const actions = {
@Guard(isAdmin, { error: 'You are not worthy!' })
deleteEverything: async () => {
// This code only runs if the guard passes
await db.deleteAllTheThings();
return { message: 'All gone!' };
}
}
`@Log`: Automatic Request Logging
Need to debug an endpoint? Add the `@Log` decorator to automatically log the incoming request and the outgoing response (or error) from your function. No more `console.log` littering your code.
import { Log } from 'vite-sveltekit-decorators';
export class PageServer {
@Log()
async load({ params }) {
// ... complex data loading
return { data };
}
}
// Console output:
// [LOG] Executing load for /my/page...
// [LOG] Finished load for /my/page in 34ms.
Before vs. After: A Side-by-Side Comparison
To truly appreciate the impact, let's compare a standard implementation with the decorator-powered one. The task is a form action that requires an authenticated admin user and validates input.
Feature | Traditional SvelteKit | With `vite-sveltekit-decorators` |
---|---|---|
Authentication | Manual `if (!locals.user)` check inside the function body. | `@Guard` decorator above the function signature. |
Authorization | Manual `if (locals.user.role !== 'ADMIN')` check. | The same `@Guard` decorator with a role-checking function. |
Input Validation | Call `request.formData()`, parse with Zod, and handle `fail()` case manually. | `@Validate` decorator handles parsing, validation, and failure response. |
Core Logic Access | Logic is mixed with boilerplate, making it harder to find. | Function body contains only core business logic. |
Readability & Intent | Intent is buried under layers of defensive code. | Intent is immediately clear from the decorators. Clean and declarative. |
Lines of Code | ~15-20 lines of boilerplate per action. | ~2 lines of declarative code per action. |
How It Works Under the Hood
There's no magic here, just modern build tooling. `vite-sveltekit-decorators` is a Vite plugin that leverages a fast code transformer like SWC or Babel during development and build steps. When it sees a decorator, it transpiles your clean, declarative code into the standard, more verbose JavaScript that the SvelteKit server understands.
For example, this:
@Guard(isAdmin)
myAction: async (event) => { /* ... */ }
Is transformed into something like this behind the scenes:
myAction: async (event) => {
if (!isAdmin(event.locals)) {
throw error(403, 'Forbidden');
}
// Original function body here
/* ... */
}
This means you get the best of both worlds: a superior developer experience with zero runtime overhead. The final production code is just as performant as if you had written the boilerplate by hand.
Getting Started: Installation and Setup
Ready to 10x your SvelteKit code? Getting started is a simple three-step process.
1. Install the package:
npm install -D vite-sveltekit-decorators
2. Update your Vite config:
// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite';
import { sveltekitDecorators } from 'vite-sveltekit-decorators/vite';
export default defineConfig({
plugins: [sveltekitDecorators(), sveltekit()],
});
3. Enable decorator support in your `tsconfig.json`:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
That's it! You can now start using the decorators in your `+page.server.ts` and `+server.ts` files.
Conclusion: A More Declarative Future for SvelteKit
Moving boilerplate from imperative code into declarative wrappers is more than just a stylistic choice. It fundamentally improves the quality of our codebase by making it easier to read, reason about, and maintain. By handling cross-cutting concerns like validation, caching, and authorization at the boundary of our functions, we keep our core business logic pure and focused.
The `vite-sveltekit-decorators` plugin is my contribution to this vision for the SvelteKit ecosystem. It's a step towards a future where we spend less time on repetitive tasks and more time building amazing features. I'm incredibly excited to see what the community builds with it. The roadmap includes decorators for rate limiting, database transactions, and more. Give it a try and let me know what you think!