Svelte 5, SvelteKit 2 and API Platform: a frontend and backend combination that makes sense
General

Svelte 5, SvelteKit 2 and API Platform: a frontend and backend combination that makes sense

admin

There is something particularly satisfying about working with a frontend and backend that have clearly defined responsibilities.

On one side, Svelte 5 and SvelteKit 2 provide a lightweight, reactive and server-aware frontend architecture. On the other, Symfony 8 and API Platform provide a structured way to expose application data, handle business operations and build APIs without turning the backend into a collection of custom controllers and repetitive boilerplate.

Put them together and you get an architecture that is both practical and pleasant to develop.

I particularly like this combination because neither side tries to do the other’s job.

Let’s look at what that means in practice.

1. Svelte 5: reactivity that is easier to reason about

The most important change in Svelte 5 is its reactivity model, built around runes.

Instead of relying on implicit component-level reactivity, we can now express our intentions directly.

The four runes worth understanding first are:

  • $state for reactive state.
  • $derived for values calculated from other state.
  • $effect for synchronizing with external systems or handling side effects.
  • $props for component properties.

Consider a simple product search.

<script lang="ts">
    let search = $state('');
    let products = $state<Product[]>([]);

    let filteredProducts = $derived(
        products.filter((product) =>
            product.name
                .toLowerCase()
                .includes(search.toLowerCase())
        )
    );
</script>

<input
    bind:value={search}
    placeholder="Search products..."
/>

{#each filteredProducts as product (product.id)}
    <div>{product.name}</div>
{/each}

There is no need to manually synchronize the search value with the filtered collection.

The relationship is explicit: when search or products changes, filteredProducts is recalculated.

This becomes particularly useful when building larger interfaces with filters, sorting, pagination and interconnected components.

Another important detail: runes are not just component syntax. They can also be used in .svelte.ts and .svelte.js modules, which makes reusable reactive logic much easier to organize.

The result is a reactivity model that scales beyond individual components.

2. SvelteKit 2: more than a way to render Svelte components

SvelteKit provides the application architecture around Svelte.

Routing, server-side rendering, data loading, form actions and server-only modules are all part of the framework.

For an application backed by API Platform, this gives us a useful separation:

  • SvelteKit handles routes, rendering and frontend-specific logic.
  • API Platform handles resources, persistence, serialization and API operations.
  • Symfony handles domain logic, security and application services.

For example, a product listing page can load its data on the server.

src/routes/products/+page.server.ts

import type { PageServerLoad } from './$types';
import { API_URL } from '$env/static/private';

export const load: PageServerLoad = async ({ fetch }) => {
    const response = await fetch(
        `${API_URL}/products`
    );

    if (!response.ok) {
        throw new Error('Unable to load products');
    }

    const collection = await response.json();

    return {
        products: collection['hydra:member']
    };
};

The API URL stays on the server, rather than being exposed as a public frontend configuration variable.

The page component receives the loaded data:

src/routes/products/+page.svelte

<script lang="ts">
    import type { PageData } from './$types';

    let { data }: { data: PageData } = $props();
</script>

<h1>Products</h1>

{#each data.products as product (product.id)}
    <article>
        <h2>{product.name}</h2>
        <p>{product.description}</p>
    </article>
{/each}

This is a straightforward example, but it demonstrates an important architectural decision: the frontend does not need to know how the backend retrieves or persists products.

It only needs a predictable API contract.

3. API Platform and Symfony 8: let the backend describe its resources

API Platform is particularly interesting because it removes much of the repetitive work involved in building REST APIs.

Instead of manually implementing collection endpoints, item endpoints, serialization and pagination, we can describe our resources and let the framework handle the standard operations.

A simplified Symfony resource could look like this:

<?php

namespace App\Entity;

use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ApiResource(operations: [
    new GetCollection(),
    new Get(),
    new Post(),
])]
class Product
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    #[ORM\Column(length: 180)]
    private string $name;

    #[ORM\Column(type: 'text', nullable: true)]
    private ?string $description = null;

    #[ORM\Column]
    private float $price;

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function getDescription(): ?string
    {
        return $this->description;
    }

    public function getPrice(): float
    {
        return $this->price;
    }
}

With Doctrine ORM configured, API Platform can provide the standard collection and item operations.

The API can expose endpoints such as:

GET    /api/products
GET    /api/products/42
POST   /api/products

Pagination, serialization and filtering can be configured through API Platform rather than implemented repeatedly in individual controllers.

For example, serialization groups let us decide exactly which properties belong in an API response:

use Symfony\Component\Serializer\Attribute\Groups;

#[ORM\Column(length: 180)]
#[Groups(['product:read', 'product:write'])]
private string $name;

#[ORM\Column]
#[Groups(['product:read'])]
private float $price;

And the resource metadata can declare those groups:

use ApiPlatform\Metadata\ApiResource;
use Symfony\Component\Serializer\Attribute\Groups;

#[ApiResource(
    normalizationContext: [
        'groups' => ['product:read']
    ],
    denormalizationContext: [
        'groups' => ['product:write']
    ]
)]

For more advanced business operations, API Platform also provides state providers and processors.

That distinction is important.

A provider is responsible for retrieving resource state. A processor handles operations that change state.

This means we can expose application-specific operations without forcing all business logic into Doctrine entities or API controllers.

Symfony remains responsible for the actual business rules.

4. Bringing everything together: a real product creation flow

Let’s connect the pieces.

Imagine an administration interface where a user creates a product.

The frontend contains a simple form:

<script lang="ts">
    let name = $state('');
    let price = $state(0);
    let loading = $state(false);
    let error = $state('');

    async function createProduct() {
        loading = true;
        error = '';

        try {
            const response = await fetch('/api/products', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/ld+json'
                },
                body: JSON.stringify({
                    name,
                    price
                })
            });

            if (!response.ok) {
                throw new Error('Product creation failed');
            }

            name = '';
            price = 0;
        } catch (e) {
            error = e instanceof Error
                ? e.message
                : 'Unexpected error';
        } finally {
            loading = false;
        }
    }
</script>

<form onsubmit={(event) => {
    event.preventDefault();
    createProduct();
}}>
    <label>
        Product name
        <input bind:value={name} required />
    </label>

    <label>
        Price
        <input
            type="number"
            bind:value={price}
            min="0"
            step="0.01"
            required
        />
    </label>

    <button type="submit" disabled={loading}>
        {loading ? 'Saving...' : 'Create product'}
    </button>

    {#if error}
        <p role="alert">{error}</p>
    {/if}
</form>

Here, /api/products represents an endpoint exposed through the application’s API gateway or reverse proxy. It can route requests to Symfony while keeping the public API contract independent of the internal backend hostname.

API Platform receives the request, validates and deserializes the data, and delegates persistence to the configured state handling.

Svelte handles the form state and user feedback.

There is no need to build a custom CRUD controller just to create a product.

Of course, production code should also include proper validation, authorization, error mapping and a deliberate decision about whether API Platform’s JSON-LD format or another supported format is the best fit.

5. Where the combination becomes really interesting

The basic CRUD example is only the beginning.

The architecture becomes more valuable when the application grows.

Filtering and pagination

API Platform can expose filters and paginated collections. SvelteKit can load the corresponding results based on URL search parameters.

This means filters can be shareable, bookmarkable and compatible with browser navigation.

Authentication and authorization

Symfony Security can remain the central authority for authentication and access control.

The frontend should reflect the permissions returned by the backend, but the backend must always enforce them. Hiding a button in Svelte is not authorization.

Depending on the application, authentication can use secure cookies, session-based authentication or token-based mechanisms. The important part is choosing one strategy deliberately and handling CSRF, CORS and credentials correctly.

Complex business operations

Not every operation should be represented as a simple entity update.

Consider approving an order, publishing an article or processing a payment.

These are business actions, not merely database updates.

API Platform processors provide a natural integration point for those operations, while Svelte can present them as clear user interactions.

Reusable frontend architecture

Svelte 5 runes make it easier to share reactive behavior between components, while SvelteKit provides a consistent place for route-specific data loading.

The same API contract can serve an administration interface, a customer-facing application and other clients.

6. A few architectural decisions worth getting right

There are some details I would address from the beginning.

First, keep the API contract independent of frontend implementation details. The backend should not return data shaped exclusively for one particular component.

Second, use SvelteKit server-side loading when data needs to be fetched securely or is required for the initial page render. Keep private credentials and internal service URLs out of browser code.

Third, avoid turning $effect into a replacement for every kind of state management. If a value can be calculated from existing state, $derived is usually the better choice.

Fourth, use API Platform’s providers and processors when the application needs custom data access or business operations. This keeps the architecture understandable as requirements become more complex.

Finally, don’t forget that Symfony 8 compatibility depends on the actual versions of API Platform and its dependencies installed in the project. The framework combination should be verified through Composer’s dependency resolution rather than assumed from the major version numbers alone.

Final thoughts

Svelte 5, SvelteKit 2, API Platform and Symfony 8 make a compelling combination because they solve different parts of the same problem.

Svelte gives us a clean and expressive way to build interfaces.

SvelteKit provides the application structure, server-side rendering and data-loading capabilities.

API Platform gives Symfony a powerful API layer with resource metadata, serialization, pagination and extensible state handling.

And Symfony remains the place where business rules, security and application logic belong.

What I find most valuable about this stack is not any individual feature. It is the fact that we can build a reasonably sophisticated application without constantly fighting the framework or introducing unnecessary layers.

The frontend stays focused on the user experience. The backend stays focused on the domain. The API connects the two.

That is a separation of responsibilities that continues to make sense as a project grows.