Skip to content

Fix: SolidStart Not Working — Routes Not Rendering, Server Functions Failing, or Hydration Errors

FixDevs · (Updated: )

Part of:  React & Frontend Errors

Quick Answer

How to fix SolidStart issues — file-based routing, server functions, createAsync data loading, middleware, sessions, and deployment configuration.

The Problem

A SolidStart route renders blank:

GET /about → blank page, no content

Or a server function throws:

'use server';

async function getUsers() {
  return db.query.users.findMany();
}
// Error: db is not defined — or — 'use server' is not recognized

Or the page hydrates incorrectly:

Hydration mismatch: server rendered "Hello" but client rendered ""

Why This Happens

SolidStart is SolidJS’s meta-framework, similar to Next.js but for Solid. It uses Vinxi under the hood:

  • SolidStart uses file-based routing in src/routes/ — files and directories map to URL paths. A missing or misplaced file means no route.
  • Server functions need 'use server' directive — code marked with 'use server' runs only on the server. Without the directive, server-only code (database, fs) is bundled into the client and crashes.
  • SolidJS reactivity is different from React — Solid uses signals and fine-grained reactivity. Destructuring props or accessing signals outside of JSX/effects breaks reactivity.
  • Hydration requires matching server/client output — if the server renders different HTML than the client expects (e.g., browser-only code runs during SSR), hydration fails.

SolidStart’s architecture is layered: SolidJS provides the reactivity model, @solidjs/router provides routing primitives, and SolidStart wires them together on top of Vinxi (the same build orchestrator used by TanStack Start). That stack matters when diagnosing errors because the failing layer dictates the fix. A “Cannot find module” error from Vinxi is a build-tool problem, not a SolidStart bug. A “use server” not recognized is a Vite plugin order issue. A blank route is usually a missing <FileRoutes /> or a misplaced file. Reading the stack trace bottom-up tells you which layer to debug.

The reactivity gotchas trip up React developers most often. In React, props are plain values and const { name } = props is harmless. In Solid, props are a proxy object — destructuring breaks reactivity because you capture the value at one point in time instead of tracking the proxy. Signals are functions, so count is the signal and count() is the current value; using {count} in JSX renders the function reference, not the number. These aren’t SolidStart problems specifically, but they surface inside SolidStart routes and look like framework bugs.

SolidStart Version History

The timeline explains why SolidStart docs from different periods look incompatible:

  • SolidStart beta (2022 — early 2024) — the framework was usable but APIs churned. The createRouteData / createServerData$ primitives were the canonical data-loading pattern, and server$ was the server-function marker. Tutorials from this era almost universally use APIs that no longer exist.
  • SolidStart 1.0 (April 2024) — first stable release. Replaced server$ with the standard 'use server' directive (aligning with React Server Components conventions), introduced query() and createAsync() as the new data primitives, and adopted Vinxi as the build system. This is the cutoff for “current” SolidStart docs.
  • SolidStart 1.0.x — 1.1.x (mid 2024) — refined the action() and useAction() API for form mutations, stabilized middleware via createMiddleware, and tightened deployment presets (Vercel, Netlify, Cloudflare Pages, Node, static).
  • SolidStart 1.2+ (late 2024 / 2026) — improved streaming SSR, better error boundaries integrated with Suspense, and a more consistent APIEvent shape for API routes. Solid 1.9+ as peer.

The practical effect: any “SolidStart not working” guide written before April 2024 will tell you to use server$ or createServerData$, which now produce confusing errors because the runtime expects 'use server' and query. If you’re following an older tutorial, swap to the 1.0+ APIs documented above. Pin @solidjs/start, @solidjs/router, and solid-js to compatible ranges — they share the runtime context object and mismatched versions cause subtle reactivity bugs.

The Vinxi dependency is worth understanding as a version axis on its own. SolidStart vendors a specific Vinxi range, and Vinxi in turn vendors specific Vite and Nitro ranges. Overriding Vite directly in your package.json (a common reflex when chasing a Vite bug) often breaks SolidStart’s plugin integration because the Vinxi-side plugin loader was compiled against the vendored Vite version. If a Vite-level fix is necessary, upgrade SolidStart to a version whose vendored Vinxi already includes the fix, rather than forcing a peer override.

Compared to its closest peers, SolidStart sits in a useful middle ground. SvelteKit owns its own router and runtime end to end; the framework feels tightly integrated but doesn’t easily share primitives with non-Svelte projects. Next.js App Router is more feature-rich but also more opinionated, with server actions and React Server Components baked deep into the framework. SolidStart, like TanStack Start, composes pieces (Solid + @solidjs/router + Vinxi + Nitro) that can be reasoned about separately. That makes debugging clearer when you know which layer is failing — and it makes upgrades smoother when each piece moves independently — at the cost of more configuration surface area at setup time.

Fix 1: Project Setup

npm init solid@latest my-app
# Select: SolidStart, TypeScript
cd my-app && npm install && npm run dev
// app.config.ts — SolidStart configuration
import { defineConfig } from '@solidjs/start/config';

export default defineConfig({
  server: {
    preset: 'node-server',  // 'vercel' | 'netlify' | 'cloudflare-pages'
  },
  vite: {
    // Vite plugins and config
  },
});
src/routes/
├── index.tsx              # /
├── about.tsx              # /about
├── [...404].tsx           # Catch-all 404
├── posts/
│   ├── index.tsx          # /posts
│   └── [id].tsx           # /posts/:id
├── (auth)/
│   ├── login.tsx          # /login (group doesn't add URL segment)
│   └── register.tsx       # /register
└── api/
    ├── users.ts           # /api/users (API route)
    └── posts/
        └── [id].ts        # /api/posts/:id

Fix 2: Routes and Data Loading

// src/routes/index.tsx — home page
import { A } from '@solidjs/router';

export default function Home() {
  return (
    <main>
      <h1>Welcome to SolidStart</h1>
      <A href="/posts">View Posts</A>
    </main>
  );
}
// src/routes/posts/index.tsx — list with server data
import { createAsync, query } from '@solidjs/router';
import { For, Suspense } from 'solid-js';

// Define a query — cached and deduplicated
const getPosts = query(async () => {
  'use server';
  return db.query.posts.findMany({
    where: eq(posts.published, true),
    orderBy: desc(posts.createdAt),
  });
}, 'posts');

// Route data — preloaded before component renders
export const route = {
  preload: () => getPosts(),
};

export default function PostsPage() {
  const posts = createAsync(() => getPosts());

  return (
    <main>
      <h1>Blog Posts</h1>
      <Suspense fallback={<p>Loading posts...</p>}>
        <For each={posts()}>
          {(post) => (
            <article>
              <A href={`/posts/${post.id}`}>
                <h2>{post.title}</h2>
              </A>
              <p>{post.excerpt}</p>
            </article>
          )}
        </For>
      </Suspense>
    </main>
  );
}
// src/routes/posts/[id].tsx — dynamic route
import { createAsync, query, useParams } from '@solidjs/router';
import { Show, Suspense } from 'solid-js';

const getPost = query(async (id: string) => {
  'use server';
  const post = await db.query.posts.findFirst({
    where: eq(posts.id, id),
  });
  if (!post) throw new Error('Post not found');
  return post;
}, 'post');

export const route = {
  preload: ({ params }: { params: { id: string } }) => getPost(params.id),
};

export default function PostPage() {
  const params = useParams();
  const post = createAsync(() => getPost(params.id));

  return (
    <Suspense fallback={<p>Loading...</p>}>
      <Show when={post()} fallback={<p>Post not found</p>}>
        {(p) => (
          <article>
            <h1>{p().title}</h1>
            <div innerHTML={p().htmlContent} />
          </article>
        )}
      </Show>
    </Suspense>
  );
}

Fix 3: Server Functions and Actions

// src/lib/server.ts — server functions
'use server';

import { db } from './db';

export async function getUsers() {
  return db.query.users.findMany();
}

export async function createUser(name: string, email: string) {
  const [user] = await db.insert(users).values({ name, email }).returning();
  return user;
}

export async function deleteUser(id: string) {
  await db.delete(users).where(eq(users.id, id));
}
// src/routes/users.tsx — using server functions with actions
import { createAsync, query, action, useAction, useSubmission } from '@solidjs/router';
import { createUser, deleteUser, getUsers } from '~/lib/server';

const getUsersQuery = query(async () => {
  'use server';
  return getUsers();
}, 'users');

// Define actions for mutations
const createUserAction = action(async (formData: FormData) => {
  'use server';
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;
  await createUser(name, email);
});

const deleteUserAction = action(async (id: string) => {
  'use server';
  await deleteUser(id);
});

export default function UsersPage() {
  const users = createAsync(() => getUsersQuery());
  const submission = useSubmission(createUserAction);

  return (
    <div>
      <h1>Users</h1>

      {/* Form with action — progressive enhancement */}
      <form action={createUserAction} method="post">
        <input name="name" placeholder="Name" required />
        <input name="email" type="email" placeholder="Email" required />
        <button type="submit" disabled={submission.pending}>
          {submission.pending ? 'Adding...' : 'Add User'}
        </button>
      </form>

      <Suspense fallback={<p>Loading...</p>}>
        <For each={users()}>
          {(user) => (
            <div>
              <span>{user.name} ({user.email})</span>
              <button onClick={() => {
                const del = useAction(deleteUserAction);
                del(user.id);
              }}>
                Delete
              </button>
            </div>
          )}
        </For>
      </Suspense>
    </div>
  );
}

Fix 4: Layouts

// src/routes/(app).tsx — layout for authenticated routes
import { A, Outlet } from '@solidjs/router';

export default function AppLayout() {
  return (
    <div class="flex min-h-screen">
      <nav class="w-64 bg-gray-900 text-white p-4">
        <h2 class="text-xl font-bold mb-4">My App</h2>
        <ul class="space-y-2">
          <li><A href="/" class="hover:text-blue-400">Home</A></li>
          <li><A href="/posts" class="hover:text-blue-400">Posts</A></li>
          <li><A href="/settings" class="hover:text-blue-400">Settings</A></li>
        </ul>
      </nav>
      <main class="flex-1 p-8">
        <Outlet />
      </main>
    </div>
  );
}

// src/app.tsx — root component
import { Router } from '@solidjs/router';
import { FileRoutes } from '@solidjs/start/router';
import { Suspense } from 'solid-js';
import './app.css';

export default function App() {
  return (
    <Router root={(props) => (
      <Suspense>{props.children}</Suspense>
    )}>
      <FileRoutes />
    </Router>
  );
}

Fix 5: API Routes

// src/routes/api/users.ts — REST API endpoint
import { json } from '@solidjs/router';
import type { APIEvent } from '@solidjs/start/server';

export async function GET(event: APIEvent) {
  const users = await db.query.users.findMany();
  return json(users);
}

export async function POST(event: APIEvent) {
  const body = await event.request.json();
  const [user] = await db.insert(users).values(body).returning();
  return json(user, { status: 201 });
}

// src/routes/api/users/[id].ts
export async function GET(event: APIEvent) {
  const id = event.params.id;
  const user = await db.query.users.findFirst({ where: eq(users.id, id) });
  if (!user) return json({ error: 'Not found' }, { status: 404 });
  return json(user);
}

export async function DELETE(event: APIEvent) {
  const id = event.params.id;
  await db.delete(users).where(eq(users.id, id));
  return new Response(null, { status: 204 });
}

Fix 6: Middleware and Sessions

// src/middleware.ts
import { createMiddleware } from '@solidjs/start/middleware';

export default createMiddleware({
  onRequest: [
    // Auth middleware
    async (event) => {
      const token = event.request.headers.get('authorization')?.replace('Bearer ', '');

      if (token) {
        try {
          const user = await verifyToken(token);
          event.locals.user = user;
        } catch {
          // Invalid token — continue without user
        }
      }
    },
    // Logging middleware
    async (event) => {
      console.log(`${event.request.method} ${event.request.url}`);
    },
  ],
});

// Access in server functions
'use server';
import { getRequestEvent } from 'solid-js/web';

export async function getCurrentUser() {
  const event = getRequestEvent();
  return event?.locals.user ?? null;
}

Still Not Working?

Blank page — check that the route file is in src/routes/ and exports a default component. Also verify src/app.tsx includes <FileRoutes /> inside a <Router>.

“use server” not recognized — ensure the string 'use server' is at the very top of the function or file. It must be a string literal, not a variable. Also check that the SolidStart Vite plugin is configured in app.config.ts.

Reactivity doesn’t work — SolidJS signals must be called as functions in JSX: {count()} not {count}. Don’t destructure props: use props.name instead of const { name } = props. Don’t access signals outside of JSX or createEffect.

Hydration mismatch — code that uses window, document, or localStorage runs on the server during SSR. Guard with import { isServer } from 'solid-js/web'; if (!isServer) { ... } or use onMount() for client-only effects.

createAsync returns undefined forever — the inner query is throwing, but Suspense is swallowing the error. Wrap the section in an <ErrorBoundary fallback={(err) => <pre>{err.toString()}</pre>}> to surface the real error. Common causes are missing env vars on the server, a database connection that silently fails, or a 'use server' function that imports a client-only module.

Build succeeds but the deployed site 500s — the Vinxi build emits different output per preset. If app.config.ts uses preset: 'node-server' but you deploy to Vercel, the entry file is in the wrong place. Match the preset to the target platform exactly, and check the platform’s function logs for the actual stack trace, not just the generic 500 page.

Mixed @solidjs/router and @solidjs/start versions — both packages share an internal context. If you bump one but not the other, queries silently fail to dedupe and useAction returns undefined. Align both to the same minor version in package.json and reinstall.

For related framework issues, see Fix: SolidJS Not Working and Fix: TanStack Start Not Working. For the closest competing meta-framework, see Fix: SvelteKit Not Working. If your Vinxi or Vite layer itself is failing, start with Fix: Vite Failed to Resolve Import.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles