12k
All articles

Server Rendering in Solid With SolidStart

Build a SolidStart v2 app with server-rendered data, query and createAsync, route preloading, server functions, rendering options, and deployment guidance.

OpenReplay Team
OpenReplay Team
Server Rendering in Solid With SolidStart

SolidStart v2 (@solidjs/start 2.x) · Solid v1 · Node.js 24+ · Vite 8

SolidStart is the full-stack framework for SolidJS. It adds file-based routing, server rendering and server functions to Solid components, and it fills the role Next.js plays for React and SvelteKit plays for Svelte.

If you already write Solid components, the hard part of going full-stack is the tutorials. Most of them still use app.config.ts and Vinxi, and neither exists in the current major version. This article builds a small SolidStart v2 app with server-loaded data and a detail page with preloading. It then covers server functions, rendering configuration, deployment, and whether the ecosystem is mature enough to choose over a React stack. It assumes you know the component-level patterns covered in SolidJS best practices.

Key Takeaways

  • SolidStart v2 became stable on August 4, 2026. It targets Solid v1, replaces Vinxi with Vite’s Environment API, requires Node.js 24+ and Vite 8, and is configured in vite.config.ts.
  • Route data in SolidStart v2 is loaded by wrapping an async function in query from @solidjs/router and reading it with createAsync.
  • A "use server" directive at the top of a query body makes that function run on the server, where it can read secrets and session data.
  • Exporting route.preload from a route module starts the route’s query before the page component renders.
  • SolidStart v2 deploys through Vite plugins (Nitro v3, Cloudflare, Netlify) and does not maintain its own hosting adapters.

What Is SolidStart and Where Does v2 Stand?

SolidStart is the meta-framework layer on top of Solid. Solid renders components. SolidStart adds routing, server rendering, data loading and a build and deploy pipeline around them.

SolidStart v2 went stable on August 4, 2026. Vinxi is gone, and v2 builds on Vite’s Environment API instead. You need Node.js 24 or later plus Vite 8, and framework settings now live in vite.config.ts rather than app.config.ts. v2 is built for Solid v1. The Solid 2.0 Release Candidate announcement says SolidStart now gets maintenance releases only, and that in Solid 2.0 a Start mode built into Solid core takes over its role. Apps already running SolidStart in production keep working. The code below targets Solid v1 and SolidStart v2. If you are upgrading an existing v1 app, follow the v1 migration guide.

Create a SolidStart Project

To scaffold a project, run the Solid initializer and pick the SolidStart template at the prompt:

npm init solid
# or: pnpm create solid
cd my-app
npm install

You configure the app in vite.config.ts. The minimal setup from the getting-started guide looks like this:

import { defineConfig } from "vite";
import { solidStart } from "@solidjs/start/config";

export default defineConfig({
  plugins: [solidStart()],
});

For production you pair solidStart() with a server plugin, for example Nitro v3. The solidStart config reference keeps Nitro settings out of solidStart(): they belong in a separate top-level nitro key.

Every file under src/routes becomes either a page or an API endpoint. The v2 API routes page explains that endpoints follow the same file-to-path rules as pages. A built-in ~ alias points at the app root, so with default settings ~/lib/db maps to src/lib/db.

Load Data on the Server With query and createAsync

In SolidStart, route data comes from an async function wrapped in query from @solidjs/router. A component then reads it through createAsync. This is the pattern the v2 data-fetching docs use. The docs also say SolidStart v2 works with either Solid Router v1 or TanStack Solid Router v1. The examples here use Solid Router.

Start with a data module at src/lib/posts.ts. It uses an in-memory array so the example runs without a database:

export type Post = { id: string; title: string; body: string };

const posts: Post[] = [
  { id: "1", title: "Hello SolidStart", body: "First post." },
  { id: "2", title: "Server functions", body: "Second post." },
];

export async function listPosts() {
  return posts.map(({ id, title }) => ({ id, title }));
}

export async function findPost(id: string) {
  return posts.find((p) => p.id === id) ?? null;
}

The posts list route:

import { For } from "solid-js";
import { createAsync, query, type RouteDefinition } from "@solidjs/router";
import { listPosts } from "~/lib/posts";

const getPostList = query(async () => {
  "use server";
  return listPosts();
}, "post-list");

export const route = {
  preload: () => getPostList(),
} satisfies RouteDefinition;

export default function PostsPage() {
  const list = createAsync(() => getPostList());
  return (
    <ul>
      <For each={list()}>
        {(post) => (
          <li>
            <a href={`/posts/${post.id}`}>{post.title}</a>
          </li>
        )}
      </For>
    </ul>
  );
}

The detail page is a dynamic route that receives an id parameter. The v2 routing docs cover the filename syntax for dynamic segments.

import { Show } from "solid-js";
import { createAsync, query, type RouteDefinition } from "@solidjs/router";
import { findPost } from "~/lib/posts";

const getPostById = query(async (id: string) => {
  "use server";
  return findPost(id);
}, "post-by-id");

export const route = {
  preload: ({ params }) => getPostById(params.id),
} satisfies RouteDefinition;

export default function PostPage(props: { params: { id: string } }) {
  const post = createAsync(() => getPostById(props.params.id));
  return (
    <Show when={post()} fallback={<p>Post not found.</p>}>
      {(p) => (
        <article>
          <h1>{p().title}</h1>
          <p>{p().body}</p>
        </article>
      )}
    </Show>
  );
}

The second argument to query is the cache key, so give each query its own. The route.preload export starts the query before the page component renders, which means data loading begins at the same moment as navigation.

Forms, Mutations and Revalidation

Writing data back to the server works differently from reading it. A mutation has to change state on the server and then tell the page that its cached reads are stale. In v2, writes go through Solid Router’s action, with a "use server" directive inside the action body when the work must happen on the server. The v2 data mutation page has the current form examples, pending and error state, and passing extra arguments with .with().

The read side of revalidation carries over from the previous section. Every query is cached under its key ("post-list" and "post-by-id" above), and createAsync re-reads when that cached entry is revalidated. When an action succeeds, Solid Router revalidates every active query on the page by default. To refresh only some of them, pass keys to revalidate: getPostList.key targets every instance of a query, and getPostById.keyFor(id) targets one. The data-fetching docs hand cache behaviour off to Solid Router, and the router’s revalidation page explains it in detail.

Clear, stable query key names pay off when you narrow revalidation. A mutation that adds a post only needs to refresh "post-list", while an edit also needs "post-by-id".

What Does “use server” Do?

Putting the "use server" directive at the top of a function body makes that function run on the server, even when it is called from a component. That is why a query marked this way can read process.env secrets and session data. The v2 data-fetching docs show this with useSession from @solidjs/start/http in their server-only example.

Put inside "use server"Keep out of it
Environment secrets (process.env.*)Browser APIs (window, localStorage, DOM)
Database and session accessUI state that lives in components
@solidjs/start/http helpersAnything that needs to run on a click without a request

In a SolidStart app, UI state belongs on the client. The server holds data, and signals and stores hold interaction state, a split covered in state management in SolidJS.

SolidStart v2 also recognizes server-only and client-only marker imports. Add one to a module, and importing it from the wrong side (server code in the client, or the reverse) breaks the dev server or the build, so the mistake never reaches users. The config reference lists @solidjs/start/http and @solidjs/start/middleware as already marked server-only. During development you also get a toolbar that shows app errors and server function calls. It never ships in a production build.

Rendering Modes in SolidStart

In SolidStart v2, rendering behaviour is configured through the solidStart() plugin and the deployment plugin you pair it with, both declared in vite.config.ts. For the conceptual trade-offs between server rendering, streaming and static generation, see the top 10 rendering patterns. This section only covers where those choices live in SolidStart.

Because app.config.ts no longer exists, any v1-era rendering flag you find in older posts does not apply as written. For the options v2 exposes, check the solidStart() config reference and the deployment-plugin docs.

When a page is rendered on the server, users see markup before the client JavaScript has hydrated it. Session replays of SSR apps make this gap visible: a user taps a button that renders correctly but does nothing, then taps again once the handlers attach.

Deploying SolidStart v2

SolidStart v2 has no hosting adapters of its own. It deploys through Vite plugins, and the v2 announcement names three supported targets: the Nitro v3 Vite plugin (still in beta per that announcement), the Cloudflare Vite plugin and the Netlify Vite plugin. The getting-started guide links to a deployment-plugins page that covers production runtime configuration for each one.

The ecosystem is the real cost of choosing SolidStart over a React stack. React has far more third-party component libraries, auth integrations and hosting templates, and Solid teams write more of that glue code themselves. The SolidJS vs React comparison sets that gap against Solid’s rendering model. Keep in mind too that SolidStart v2 is in maintenance while Solid 2.0 moves toward Start mode.

Conclusion

SolidStart v2 is a stable, Vite-native way to add server rendering to Solid v1 apps. Data loading comes down to three pieces: query, createAsync and a "use server" directive, with route.preload for early fetching. The next step is to scaffold a project with npm init solid, add the two post routes above, and then wire up a mutation using the data-mutation section of the v2 docs.

FAQs

Does SolidStart v2 work with Solid 2.0?

No. SolidStart v2 is built for Solid 1.x, and no SolidStart release targets Solid 2.0. The Solid 2.0 Release Candidate retires SolidStart and moves its pieces elsewhere: Solid core now handles server functions, Start mode does the serving, and a separate router-neutral package handles file-system routing. Existing SolidStart apps keep working, SolidStart still receives maintenance releases, and an official migration guide covers the move to Start mode.

What is the difference between TanStack Start and SolidStart with TanStack Router?

They are separate options. SolidStart v2 is router agnostic and runs with either the official Solid Router v1 or TanStack Solid Router v1, so you can use TanStack routing without leaving SolidStart. TanStack Start is a separate metaframework with a Solid variant, published as @tanstack/solid-start, and it already has a Solid 2.0 beta release. For Solid 2.0, the fullstack-tanstack starter combines Start mode with TanStack Router and TanStack Query.

What are single-flight mutations in SolidStart?

Single-flight mutations send the updated data back in the same server response as the mutation itself. When a server-side action changes data the current view has preloaded, or redirects to a page whose data is preloaded, SolidStart revalidates that data on the server and delivers it on the same round trip. The client does not have to make a second set of requests, so there is no request waterfall after a write.

What changes when upgrading a SolidStart v1 app to v2?

Upgrading requires Node.js 24 and Vite 8. Framework configuration moves from app.config.ts into vite.config.ts, where solidStart() is registered as a Vite plugin, and Vite's Environment API replaces Vinxi. The official migration guide also walks through what changed in import paths, TypeScript environment types, the server runtime and middleware. Review v1 import paths and middleware code against the guide rather than copying them over as they are.

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.