.claude/skills/solidjs/solid-impl/solid-impl-solidstart/SKILL.md
Use when building SolidStart applications with SSR, file-based routing, or server functions. Prevents incorrect 'use server' placement, broken hydration, and misuse of query/createAsync patterns. Covers file-based routing, data loading with query and createAsync, server functions, actions with form integration, SSR streaming, hydration, and API routes. Keywords: SolidStart, use server, createAsync, query, file-based routing, SSR, hydration, API routes, Vinxi, Nitro.
npx skillsauth add OpenAEC-Foundation/OpenAEC-Workspace-Composer solid-impl-solidstartInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
| Layer | Technology | Role | |-------|-----------|------| | SolidJS | Fine-grained reactivity | Component rendering, signals, stores | | Vite | Bundler | Dev server, HMR, production builds | | Nitro | Server runtime | Deployment adapters, server functions | | Vinxi | CLI + config | Combines Vite + Nitro into unified tooling |
src/
├── routes/ # File-based routing (ALWAYS here)
│ ├── index.tsx # / route
│ ├── about.tsx # /about route
│ └── api/ # API routes
├── entry-client.tsx # Client hydration entry
├── entry-server.tsx # Server request handler
└── app.tsx # Root component (Router + FileRoutes)
import { Suspense } from "solid-js";
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
export default function App() {
return (
<Router root={(props) => <Suspense>{props.children}</Suspense>}>
<FileRoutes />
</Router>
);
}
ALWAYS make "use server" functions async -- the directive requires the function to return a Promise. Non-async server functions cause runtime errors.
ALWAYS throw redirect() from server functions -- NEVER return redirect(). Redirect is implemented as a thrown response, not a return value. Returning it does nothing.
NEVER use getServerSideProps, useLoaderData, or any Next.js/React Router data loading patterns. SolidStart uses query() + createAsync() exclusively.
NEVER use useEffect for data fetching -- SolidStart has NO equivalent of React's useEffect data fetching pattern. ALWAYS use query() + createAsync().
NEVER use element={<Component />} on Route -- ALWAYS use component={Component}. The React pattern creates the component immediately, breaking Solid's deferred rendering.
NEVER use the deprecated 0.x APIs (createServerData$, createServerAction$) -- ALWAYS use query(), action(), and "use server" from SolidStart 1.0+.
| File Pattern | URL | Description |
|-------------|-----|-------------|
| routes/index.tsx | / | Home route |
| routes/about.tsx | /about | Static route |
| routes/blog/article.tsx | /blog/article | Nested route |
| routes/blog.tsx + routes/blog/ | Layout wrapper | Layout for /blog/* children |
| routes/users/[id].tsx | /users/:id | Dynamic parameter |
| routes/users/[[id]].tsx | /users or /users/:id | Optional parameter |
| routes/docs/[...path].tsx | /docs/* | Catch-all parameter |
| routes/(admin)/dashboard.tsx | /dashboard | Route group (no URL segment) |
| routes/api/users.ts | /api/users | API route (no default export) |
import { useParams } from "@solidjs/router";
// routes/users/[id].tsx
export default function UserPage() {
const params = useParams();
return <div>User: {params.id}</div>;
}
// routes/docs/[...path].tsx -- catch-all
export default function DocsPage() {
const params = useParams();
// params.path = "guides/getting-started" (slash-delimited)
return <div>Doc: {params.path}</div>;
}
// routes/blog.tsx -- layout wrapper for /blog/*
import type { RouteSectionProps } from "@solidjs/router";
export default function BlogLayout(props: RouteSectionProps) {
return (
<div>
<nav>Blog Navigation</nav>
{props.children}
</div>
);
}
Need server-side data?
├─ YES: Is it a read operation (query)?
│ ├─ YES: Use query() + createAsync()
│ │ └─ Need fine-grained store updates? → Use createAsyncStore()
│ └─ NO: It's a mutation → Use action() (see Actions section)
└─ NO: Client-only data
└─ Use createSignal/createResource directly
import { query } from "@solidjs/router";
const getUser = query(async (id: string) => {
"use server";
const user = await db.getUser(id);
if (!user) throw new Error("User not found");
return user;
}, "user");
import { createAsync } from "@solidjs/router";
import { Suspense } from "solid-js";
export default function UserPage() {
const params = useParams();
const user = createAsync(() => getUser(params.id));
return (
<Suspense fallback={<p>Loading...</p>}>
<h1>{user()?.name}</h1>
</Suspense>
);
}
import type { RouteDefinition } from "@solidjs/router";
export const route = {
preload({ params }) {
getUser(params.id); // fire-and-forget, warms cache
},
} satisfies RouteDefinition;
Need to mutate server data?
├─ From a <form>?
│ ├─ YES: Use action={myAction} method="post"
│ │ └─ Need extra args? → Use action={myAction.with(extraArg)}
│ └─ NO: Programmatic mutation
│ └─ Use useAction(myAction) for imperative calls
└─ Track submission state?
├─ Single submission → useSubmission(myAction)
└─ Multiple concurrent → useSubmissions(myAction)
import { action, redirect } from "@solidjs/router";
const addTodo = action(async (formData: FormData) => {
"use server";
const title = formData.get("title") as string;
await db.addTodo(title);
throw redirect("/todos"); // ALWAYS throw, NEVER return
}, "addTodo");
export default function NewTodo() {
return (
<form action={addTodo} method="post">
<input name="title" required />
<button type="submit">Add</button>
</form>
);
}
import { action, useAction } from "@solidjs/router";
const toggleTodo = action(async (id: string) => {
"use server";
await db.toggleTodo(id);
}, "toggleTodo");
function TodoItem(props: { id: string; done: boolean }) {
const doToggle = useAction(toggleTodo);
return (
<button onClick={() => doToggle(props.id)}>
{props.done ? "Undo" : "Done"}
</button>
);
}
import { useSubmission } from "@solidjs/router";
function TodoForm() {
const submission = useSubmission(addTodo);
return (
<form action={addTodo} method="post">
<input name="title" />
<button disabled={submission.pending}>
{submission.pending ? "Adding..." : "Add"}
</button>
</form>
);
}
import { redirect, reload, revalidate } from "@solidjs/router";
const updateUser = action(async (id: string, formData: FormData) => {
"use server";
await db.updateUser(id, Object.fromEntries(formData));
// Option 1: Redirect (ALWAYS throw)
throw redirect("/users");
// Option 2: Revalidate specific query + redirect
throw redirect("/users", { revalidate: getUser.keyFor(id) });
// Option 3: Reload current page
throw reload({ revalidate: getUser.key });
}, "updateUser");
const getData = async (id: string) => {
"use server";
return db.getData(id); // Runs server-only, compiled to RPC
};
// src/lib/server/queries.ts
"use server";
export async function getUsers() {
return db.getUsers();
}
export async function getProduct(id: string) {
return db.getProduct(id);
}
Arguments and return values MUST be serializable (SolidStart uses Seroval). NEVER pass DOM nodes, functions, or class instances through server functions.
| Mode | Function | Use Case |
|------|----------|----------|
| Sync SSR | renderToString | Static pages, no async data |
| Streaming SSR | renderToStream | Dynamic pages with Suspense boundaries |
| Client hydration | hydrate | Attach reactivity to server-rendered HTML |
import { renderToStream } from "solid-js/web";
const stream = renderToStream(() => <App />, {
onCompleteShell() {
// Shell rendered, Suspense fallbacks in place
},
onCompleteAll() {
// All async content resolved
},
});
stream.pipe(res); // Node.js
stream.pipeTo(writable); // Web Streams
ALWAYS include <HydrationScript /> once in the HTML head. It captures user events (click, input) before Solid loads, then replays them after hydration.
import { isServer } from "solid-js/web";
if (isServer) {
// Tree-shaken from client bundle at build time
}
API routes export named HTTP method handlers (NOT a default component):
// routes/api/users/[id].ts
import type { APIEvent } from "@solidjs/start/server";
export async function GET({ params }: APIEvent) {
const user = await db.getUser(params.id);
if (!user) return new Response("Not found", { status: 404 });
return user; // Auto-serialized to JSON
}
export async function DELETE({ params }: APIEvent) {
await db.deleteUser(params.id);
return new Response(null, { status: 204 });
}
API routes are prioritized over UI routes at the same path.
| 0.x (Deprecated) | 1.0 (Current) |
|-------------------|----------------|
| createServerData$ | query() + createAsync() |
| createServerAction$ | action() |
| createRouteData | query() + createAsync() |
| useRouteData() | createAsync(() => myQuery()) |
| server$() wrapper | "use server" directive |
development
Use when integrating Vite with a backend framework, rendering Vite assets from server-side templates, or setting up dev/production HTML serving. Prevents incorrect manifest.json traversal and missing CSS chunk resolution in production. Covers build.manifest configuration, .vite/manifest.json structure, ManifestChunk properties, dev mode HTML setup, production rendering, CSS/JS chunk resolution, and modulepreload polyfill. Keywords: backend integration, manifest.json, ManifestChunk, Django, Laravel, Rails, modulepreload.
development
Use when encountering dev server startup failures, HMR issues, proxy errors, CORS blocks, or module not found errors during development. Prevents misconfiguring server.hmr behind reverse proxies and forgetting appType: 'custom' in middleware mode. Covers HMR full-reload debugging, proxy configuration, CORS setup, HTTPS certificates, server.fs.strict violations, port conflicts, WebSocket failures, file watcher issues, and middleware mode. Keywords: dev server, HMR, proxy, CORS, HTTPS, WebSocket, port conflict, server.fs.strict, middleware mode, file watcher.
development
Use when encountering pre-bundling errors, dependency resolution failures, stale cache issues, or slow development server startup. Prevents excluding CJS dependencies from pre-bundling (which breaks runtime module resolution) and misconfiguring optimizeDeps. Covers CJS/ESM conversion failures, missing dependency auto-discovery, optimizeDeps configuration, monorepo linked dependencies, cache invalidation, browser cache staleness, and large dependency tree performance. Keywords: pre-bundling, optimizeDeps, CJS, ESM, cache, dependency resolution, monorepo, node_modules/.vite.
development
Use when encountering Vite build failures, chunk size warnings, or version-specific build errors. Prevents the common mistake of using deprecated rollupOptions in v8 or misconfiguring build targets and minifiers. Covers Rolldown/Rollup bundling failures, CSS minification errors, sourcemap problems, library mode build failures, BundleError handling, and asset processing errors. Keywords: build error, Rolldown, chunk size, sourcemap, library mode, minify, BundleError, rollupOptions, build.target.