diff --git a/docs/start/framework/react/fetching-external-api.md b/docs/start/framework/react/fetching-external-api.md index 16f49f6ce87..a9690ca2546 100644 --- a/docs/start/framework/react/fetching-external-api.md +++ b/docs/start/framework/react/fetching-external-api.md @@ -61,7 +61,7 @@ At this point, the project structure should look like this: │ ├── routeTree.gen.ts # Generated route tree │ └── styles.css # Global styles ├── public/ # Static assets -├── app.config.ts # TanStack Start configuration +├── vite.config.ts # TanStack Start configuration ├── package.json # Project dependencies └── tsconfig.json # TypeScript configuration diff --git a/docs/start/framework/react/path-aliases.md b/docs/start/framework/react/path-aliases.md index 0eafa38faf9..2579a42a2a1 100644 --- a/docs/start/framework/react/path-aliases.md +++ b/docs/start/framework/react/path-aliases.md @@ -26,11 +26,11 @@ After updating your `tsconfig.json` file, you'll need to install the `vite-tscon npm install -D vite-tsconfig-paths ``` -Now, you'll need to update your `app.config.ts` file to include the following: +Now, you'll need to update your `vite.config.ts` file to include the following: ```ts -// app.config.ts -import { defineConfig } from '@tanstack/react-start/config' +// vite.config.ts +import { defineConfig } from 'vite' import viteTsConfigPaths from 'vite-tsconfig-paths' export default defineConfig({ diff --git a/docs/start/framework/react/reading-writing-file.md b/docs/start/framework/react/reading-writing-file.md index 784ba21aa61..e07bb5dca1e 100644 --- a/docs/start/framework/react/reading-writing-file.md +++ b/docs/start/framework/react/reading-writing-file.md @@ -79,7 +79,7 @@ At this point, the project structure should look like this - │ ├── ssr.tsx # Server-side rendering │ └── styles.css # Global styles ├── public/ # Static assets -├── app.config.ts # TanStack Start configuration +├── vite.config.ts # TanStack Start configuration ├── package.json # Project dependencies └── tsconfig.json # TypeScript configuration ``` diff --git a/docs/start/framework/react/server-routes.md b/docs/start/framework/react/server-routes.md index 80abd9dcc72..2fe5431071b 100644 --- a/docs/start/framework/react/server-routes.md +++ b/docs/start/framework/react/server-routes.md @@ -453,11 +453,7 @@ Sometimes you may need to set headers in the response. You can do this by either ```ts // routes/hello.ts import { createFileRoute } from '@tanstack/react-router' - ``` - -> > > > > > > 582e8c7a1 (docs: Start overhaul) -> > > > > > > export const Route = createFileRoute('/hello')({ - + export const Route = createFileRoute('/hello')({ server: { handlers: { GET: async ({ request }) => { @@ -469,13 +465,10 @@ Sometimes you may need to set headers in the response. You can do this by either }, }, }, - -}) - -// Visit /hello to see the response -// Hello, World! - -```` + }) + // Visit /hello to see the response + // Hello, World! + ``` - Or using the `setResponseHeaders` helper function from `@tanstack/react-start/server`. @@ -496,4 +489,4 @@ export const Route = createFileRoute('/hello')({ }, }, }) -```` +``` diff --git a/docs/start/framework/solid/authentication.md b/docs/start/framework/solid/authentication.md index 7a6f8f01d61..276381fb08e 100644 --- a/docs/start/framework/solid/authentication.md +++ b/docs/start/framework/solid/authentication.md @@ -1,9 +1,601 @@ --- -ref: docs/start/framework/react/authentication.md -replace: - { - '@tanstack/react-start': '@tanstack/solid-start', - 'React': 'SolidJS', - 'react-router': 'solid-router', - } +id: authentication +title: Authentication --- + +This guide covers authentication patterns and shows how to implement your own authentication system with TanStack Start. + +> **📋 Before You Start:** Check our [Authentication Overview](../authentication-overview.md) for all available options including partner solutions and hosted services. + +## Authentication Approaches + +You have several options for authentication in your TanStack Start application: + +**Hosted Solutions:** + +1. **[Clerk](https://clerk.dev)** - Complete authentication platform with UI components +2. **[WorkOS](https://workos.com)** - Enterprise-focused with SSO and compliance features +3. **[Better Auth](https://www.better-auth.com/)** - Open-source TypeScript library + +**DIY Implementation Benefits:** + +- **Full Control**: Complete customization over authentication flow +- **No Vendor Lock-in**: Own your authentication logic and user data +- **Custom Requirements**: Implement specific business logic or compliance needs +- **Cost Control**: No per-user pricing or usage limits + +Authentication involves many considerations including password security, session management, rate limiting, CSRF protection, and various attack vectors. + +## Core Concepts + +### Authentication vs Authorization + +- **Authentication**: Who is this user? (Login/logout) +- **Authorization**: What can this user do? (Permissions/roles) + +TanStack Start provides the tools for both through server functions, sessions, and route protection. + +## Essential Building Blocks + +### 1. Server Functions for Authentication + +Server functions handle sensitive authentication logic securely on the server: + +```tsx +import { createServerFn } from '@tanstack/solid-start' +import { redirect } from '@tanstack/solid-router' + +// Login server function +export const loginFn = createServerFn({ method: 'POST' }) + .inputValidator((data: { email: string; password: string }) => data) + .handler(async ({ data }) => { + // Verify credentials (replace with your auth logic) + const user = await authenticateUser(data.email, data.password) + + if (!user) { + return { error: 'Invalid credentials' } + } + + // Create session + const session = await useAppSession() + await session.update({ + userId: user.id, + email: user.email, + }) + + // Redirect to protected area + throw redirect({ to: '/dashboard' }) + }) + +// Logout server function +export const logoutFn = createServerFn({ method: 'POST' }).handler(async () => { + const session = await useAppSession() + await session.clear() + throw redirect({ to: '/' }) +}) + +// Get current user +export const getCurrentUserFn = createServerFn({ method: 'GET' }).handler( + async () => { + const session = await useAppSession() + const userId = session.get('userId') + + if (!userId) { + return null + } + + return await getUserById(userId) + }, +) +``` + +### 2. Session Management + +TanStack Start provides secure HTTP-only cookie sessions: + +```tsx +// utils/session.ts +import { useSession } from '@tanstack/solid-start/server' + +type SessionData = { + userId?: string + email?: string + role?: string +} + +export function useAppSession() { + return useSession({ + // Session configuration + name: 'app-session', + password: process.env.SESSION_SECRET!, // At least 32 characters + // Optional: customize cookie settings + cookie: { + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + httpOnly: true, + }, + }) +} +``` + +### 3. Authentication Context + +Share authentication state across your application: + +```tsx +// contexts/auth.tsx +import { createContext, useContext } from 'solid-js' +import { useServerFn } from '@tanstack/solid-start' +import { getCurrentUserFn } from '../server/auth' + +type User = { + id: string + email: string + role: string +} + +type AuthContextType = { + user: User | null + isLoading: boolean + refetch: () => void +} + +const AuthContext = createContext(undefined) + +export function AuthProvider(props) { + const { data: user, isLoading, refetch } = useServerFn(getCurrentUserFn) + + return ( + + {props.children} + + ) +} + +export function useAuth() { + const context = useContext(AuthContext) + if (!context) { + throw new Error('useAuth must be used within AuthProvider') + } + return context +} +``` + +### 4. Route Protection + +Protect routes using `beforeLoad`: + +```tsx +// routes/_authed.tsx - Layout route for protected pages +import { createFileRoute, redirect } from '@tanstack/solid-router' +import { getCurrentUserFn } from '../server/auth' + +export const Route = createFileRoute('/_authed')({ + beforeLoad: async ({ location }) => { + const user = await getCurrentUserFn() + + if (!user) { + throw redirect({ + to: '/login', + search: { redirect: location.href }, + }) + } + + // Pass user to child routes + return { user } + }, +}) +``` + +```tsx +// routes/_authed/dashboard.tsx - Protected route +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/_authed/dashboard')({ + component: DashboardComponent, +}) + +function DashboardComponent() { + const context = Route.useRouteContext() + + return ( +
+

Welcome, {context().user.email}!

+ {/* Dashboard content */} +
+ ) +} +``` + +## Implementation Patterns + +### Basic Email/Password Authentication + +```tsx +// server/auth.ts +import bcrypt from 'bcryptjs' +import { createServerFn } from '@tanstack/solid-start' + +// User registration +export const registerFn = createServerFn({ method: 'POST' }) + .inputValidator( + (data: { email: string; password: string; name: string }) => data, + ) + .handler(async ({ data }) => { + // Check if user exists + const existingUser = await getUserByEmail(data.email) + if (existingUser) { + return { error: 'User already exists' } + } + + // Hash password + const hashedPassword = await bcrypt.hash(data.password, 12) + + // Create user + const user = await createUser({ + email: data.email, + password: hashedPassword, + name: data.name, + }) + + // Create session + const session = await useAppSession() + await session.update({ userId: user.id }) + + return { success: true, user: { id: user.id, email: user.email } } + }) + +async function authenticateUser(email: string, password: string) { + const user = await getUserByEmail(email) + if (!user) return null + + const isValid = await bcrypt.compare(password, user.password) + return isValid ? user : null +} +``` + +### Role-Based Access Control (RBAC) + +```tsx +// utils/auth.ts +export const roles = { + USER: 'user', + ADMIN: 'admin', + MODERATOR: 'moderator', +} as const + +type Role = (typeof roles)[keyof typeof roles] + +export function hasPermission(userRole: Role, requiredRole: Role): boolean { + const hierarchy = { + [roles.USER]: 0, + [roles.MODERATOR]: 1, + [roles.ADMIN]: 2, + } + + return hierarchy[userRole] >= hierarchy[requiredRole] +} + +// Protected route with role check +export const Route = createFileRoute('/_authed/admin/')({ + beforeLoad: async ({ context }) => { + if (!hasPermission(context.user.role, roles.ADMIN)) { + throw redirect({ to: '/unauthorized' }) + } + }, +}) +``` + +### Social Authentication Integration + +```tsx +// Example with OAuth providers +export const authProviders = { + google: { + clientId: process.env.GOOGLE_CLIENT_ID!, + redirectUri: `${process.env.APP_URL}/auth/google/callback`, + }, + github: { + clientId: process.env.GITHUB_CLIENT_ID!, + redirectUri: `${process.env.APP_URL}/auth/github/callback`, + }, +} + +export const initiateOAuthFn = createServerFn({ method: 'POST' }) + .inputValidator((data: { provider: 'google' | 'github' }) => data) + .handler(async ({ data }) => { + const provider = authProviders[data.provider] + const state = generateRandomState() + + // Store state in session for CSRF protection + const session = await useAppSession() + await session.update({ oauthState: state }) + + // Generate OAuth URL + const authUrl = generateOAuthUrl(provider, state) + + throw redirect({ href: authUrl }) + }) +``` + +### Password Reset Flow + +```tsx +// Password reset request +export const requestPasswordResetFn = createServerFn({ method: 'POST' }) + .inputValidator((data: { email: string }) => data) + .handler(async ({ data }) => { + const user = await getUserByEmail(data.email) + if (!user) { + // Don't reveal if email exists + return { success: true } + } + + const token = generateSecureToken() + const expires = new Date(Date.now() + 60 * 60 * 1000) // 1 hour + + await savePasswordResetToken(user.id, token, expires) + await sendPasswordResetEmail(user.email, token) + + return { success: true } + }) + +// Password reset confirmation +export const resetPasswordFn = createServerFn({ method: 'POST' }) + .inputValidator((data: { token: string; newPassword: string }) => data) + .handler(async ({ data }) => { + const resetToken = await getPasswordResetToken(data.token) + + if (!resetToken || resetToken.expires < new Date()) { + return { error: 'Invalid or expired token' } + } + + const hashedPassword = await bcrypt.hash(data.newPassword, 12) + await updateUserPassword(resetToken.userId, hashedPassword) + await deletePasswordResetToken(data.token) + + return { success: true } + }) +``` + +## Security Best Practices + +### 1. Password Security + +```tsx +// Use strong hashing (bcrypt, scrypt, or argon2) +import bcrypt from 'bcryptjs' + +const saltRounds = 12 // Adjust based on your security needs +const hashedPassword = await bcrypt.hash(password, saltRounds) +``` + +### 2. Session Security + +```tsx +// Use secure session configuration +export function useAppSession() { + return useSession({ + name: 'app-session', + password: process.env.SESSION_SECRET!, // 32+ characters + cookie: { + secure: process.env.NODE_ENV === 'production', // HTTPS only in production + sameSite: 'lax', // CSRF protection + httpOnly: true, // XSS protection + maxAge: 7 * 24 * 60 * 60, // 7 days + }, + }) +} +``` + +### 3. Rate Limiting + +```tsx +// Simple in-memory rate limiting (use Redis in production) +const loginAttempts = new Map() + +export const rateLimitLogin = (ip: string): boolean => { + const now = Date.now() + const attempts = loginAttempts.get(ip) + + if (!attempts || now > attempts.resetTime) { + loginAttempts.set(ip, { count: 1, resetTime: now + 15 * 60 * 1000 }) // 15 min + return true + } + + if (attempts.count >= 5) { + return false // Too many attempts + } + + attempts.count++ + return true +} +``` + +### 4. Input Validation + +```tsx +import { z } from 'zod' + +const loginSchema = z.object({ + email: z.string().email().max(255), + password: z.string().min(8).max(100), +}) + +export const loginFn = createServerFn({ method: 'POST' }) + .inputValidator((data) => loginSchema.parse(data)) + .handler(async ({ data }) => { + // data is now validated + }) +``` + +## Testing Authentication + +### Unit Testing Server Functions + +```tsx +// __tests__/auth.test.ts +import { describe, it, expect, beforeEach } from 'vitest' +import { loginFn } from '../server/auth' + +describe('Authentication', () => { + beforeEach(async () => { + await setupTestDatabase() + }) + + it('should login with valid credentials', async () => { + const result = await loginFn({ + data: { email: 'test@example.com', password: 'password123' }, + }) + + expect(result.error).toBeUndefined() + expect(result.user).toBeDefined() + }) + + it('should reject invalid credentials', async () => { + const result = await loginFn({ + data: { email: 'test@example.com', password: 'wrongpassword' }, + }) + + expect(result.error).toBe('Invalid credentials') + }) +}) +``` + +### Integration Testing + +```tsx +// __tests__/auth-flow.test.tsx +import { render, screen, fireEvent, waitFor } from '@solidjs/testing-library' +import { RouterProvider, createMemoryHistory } from '@tanstack/solid-router' +import { router } from '../router' + +describe('Authentication Flow', () => { + it('should redirect to login when accessing protected route', async () => { + const history = createMemoryHistory() + history.push('/dashboard') // Protected route + + render() + + await waitFor(() => { + expect(screen.getByText('Login')).toBeInTheDocument() + }) + }) +}) +``` + +## Common Patterns + +### Loading States + +```tsx +function LoginForm() { + const [isLoading, setIsLoading] = createSignal(false) + const loginMutation = useServerFn(loginFn) + + const handleSubmit = async (data: LoginData) => { + setIsLoading(true) + try { + await loginMutation.mutate(data) + } catch (error) { + // Handle error + } finally { + setIsLoading(false) + } + } + + return ( +
+ {/* Form fields */} + +
+ ) +} +``` + +### Remember Me Functionality + +```tsx +export const loginFn = createServerFn({ method: 'POST' }) + .inputValidator( + (data: { email: string; password: string; rememberMe?: boolean }) => data, + ) + .handler(async ({ data }) => { + const user = await authenticateUser(data.email, data.password) + if (!user) return { error: 'Invalid credentials' } + + const session = await useAppSession() + await session.update( + { userId: user.id }, + { + // Extend session if remember me is checked + maxAge: data.rememberMe ? 30 * 24 * 60 * 60 : undefined, // 30 days vs session + }, + ) + + return { success: true } + }) +``` + +## Migration from Other Solutions + +### From Client-Side Auth + +If you're migrating from client-side authentication (localStorage, context only): + +1. Move authentication logic to server functions +2. Replace localStorage with server sessions +3. Update route protection to use `beforeLoad` +4. Add proper security headers and CSRF protection + +### From Other Frameworks + +- **Next.js**: Replace API routes with server functions, migrate NextAuth sessions +- **Remix**: Convert loaders/actions to server functions, adapt session patterns +- **SvelteKit**: Move form actions to server functions, update route protection + +## Production Considerations + +When choosing your authentication approach, consider these factors: + +### Hosted vs DIY Comparison + +**Hosted Solutions (Clerk, WorkOS, Better Auth):** + +- Pre-built security measures and regular updates +- UI components and user management features +- Compliance certifications and audit trails +- Support and documentation +- Per-user or subscription pricing + +**DIY Implementation:** + +- Complete control over implementation and data +- No ongoing subscription costs +- Custom business logic and workflows +- Responsibility for security updates and monitoring +- Need to handle edge cases and attack vectors + +### Security Considerations + +Authentication systems need to handle various security aspects: + +- Password hashing and timing attack prevention +- Session management and fixation protection +- CSRF and XSS protection +- Rate limiting and brute force prevention +- OAuth flow security +- Compliance requirements (GDPR, CCPA, etc.) + +## Next Steps + +When implementing authentication, consider: + +- **Security Review**: Review your implementation for security best practices +- **Performance**: Add caching for user lookups and session validation +- **Monitoring**: Add logging and monitoring for authentication events +- **Compliance**: Ensure compliance with relevant regulations if storing personal data + +For other authentication approaches, check the [Authentication Overview](../authentication-overview.md). For specific integration help, see the [How-to Guides](/router/latest/docs/framework/solid/how-to/README.md#authentication) or explore our [working examples](https://github.com/TanStack/router/tree/main/examples/solid). diff --git a/docs/start/framework/solid/build-from-scratch.md b/docs/start/framework/solid/build-from-scratch.md index 08a79d47883..61242ce6e90 100644 --- a/docs/start/framework/solid/build-from-scratch.md +++ b/docs/start/framework/solid/build-from-scratch.md @@ -1,21 +1,260 @@ --- -ref: docs/start/framework/react/build-from-scratch.md -replace: - { - '@tanstack/react-start': '@tanstack/solid-start', - 'react-router': 'solid-router', - 'react react-dom': 'solid-js', - "Alternatively, you can also use `@vitejs/plugin-react-oxc` or `@vitejs/plugin-react-swc`.\n": '', - '@vitejs/plugin-react': 'vite-plugin-solid', - '@types/react @types/react-dom ': '', - '"jsx": "react-jsx"': "\"jsx\": \"preserve\",\n \"jsxImportSource\": \"solid-js\"", - 'import viteReact': 'import viteSolid', - "viteReact\\(\\)": 'viteSolid({ssr:true})', - "type { ReactNode } from 'react'": "type * as Solid from 'solid-js'", - 'ReactNode': 'Solid.JSX.Element', - '{state}': '{state()}', - " \n \n \n \n \n {children}\n \n \n ": " <>\n \n {children}\n \n ", - "react's": "solid's", - 'React': 'SolidJS', - } +id: build-from-scratch +title: Build a Project from Scratch --- + +> [!NOTE] +> If you chose to quick start with an example or cloned project, you can skip this guide and move on to the [Routing](../routing) guide. + +_So you want to build a TanStack Start project from scratch?_ + +This guide will help you build a **very** basic TanStack Start web application. Together, we will use TanStack Start to: + +- Serve an index page +- Display a counter +- Increment the counter on the server and client + +Let's create a new project directory and initialize it. + +```shell +mkdir myApp +cd myApp +npm init -y +``` + +> [!NOTE] > We use `npm` in all of these examples, but you can use your package manager of choice instead. + +## TypeScript Configuration + +We highly recommend using TypeScript with TanStack Start. Create a `tsconfig.json` file with at least the following settings: + +```json +{ + "compilerOptions": { + "jsx": "preserve", + "jsxImportSource": "solid-js", + "moduleResolution": "Bundler", + "module": "ESNext", + "target": "ES2022", + "skipLibCheck": true, + "strictNullChecks": true + } +} +``` + +> [!NOTE] > Enabling `verbatimModuleSyntax` can result in server bundles leaking into client bundles. It is recommended to keep this option disabled. + +## Install Dependencies + +TanStack Start is powered by [Vite](https://vite.dev/) and [TanStack Router](https://tanstack.com/router) and requires them as dependencies. + +To install them, run: + +```shell +npm i @tanstack/solid-start @tanstack/solid-router vite +``` + +You'll also need SolidJS: + +```shell +npm i solid-js vite-plugin-solid +``` + +and some TypeScript: + +```shell +npm i -D typescript @types/node vite-tsconfig-paths +``` + +## Update Configuration Files + +We'll then update our `package.json` to use Vite's CLI and set `"type": "module"`: + +```json +{ + // ... + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build" + } +} +``` + +Then configure TanStack Start's Vite plugin in `vite.config.ts`: + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import tsConfigPaths from 'vite-tsconfig-paths' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import viteSolid from 'vite-plugin-solid' + +export default defineConfig({ + server: { + port: 3000, + }, + plugins: [ + tsConfigPaths(), + tanstackStart(), + // solid's vite plugin must come after start's vite plugin + viteSolid({ ssr: true }), + ], +}) +``` + +## Add the Basic Templating + +There are 2 required files for TanStack Start usage: + +1. The router configuration +2. The root of your application + +Once configuration is done, we'll have a file tree that looks like the following: + +``` +. +├── src/ +│ ├── routes/ +│ │ └── `__root.tsx` +│ ├── `router.tsx` +│ ├── `routeTree.gen.ts` +├── `vite.config.ts` +├── `package.json` +└── `tsconfig.json` +``` + +## The Router Configuration + +This is the file that will dictate the behavior of TanStack Router used within Start. Here, you can configure everything +from the default [preloading functionality](/router/latest/docs/framework/solid/guide/preloading) to [caching staleness](/router/latest/docs/framework/solid/guide/data-loading). + +> [!NOTE] +> You won't have a `routeTree.gen.ts` file yet. This file will be generated when you run TanStack Start for the first time. + +```tsx +// src/router.tsx +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +export function getRouter() { + const router = createRouter({ + routeTree, + scrollRestoration: true, + }) + + return router +} +``` + +## The Root of Your Application + +Finally, we need to create the root of our application. This is the entry point for all other routes. The code in this file will wrap all other routes in the application. + +```tsx +// src/routes/__root.tsx +/// +import type * as Solid from 'solid-js' +import { + Outlet, + createRootRoute, + HeadContent, + Scripts, +} from '@tanstack/solid-router' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { + charSet: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + { + title: 'TanStack Start Starter', + }, + ], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + ) +} + +function RootDocument({ children }: Readonly<{ children: Solid.JSX.Element }>) { + return ( + <> + + {children} + + + ) +} +``` + +## Writing Your First Route + +Now that we have the basic templating setup, we can write our first route. This is done by creating a new file in the `src/routes` directory. + +```tsx +import * as fs from 'node:fs' +import { createFileRoute, useRouter } from '@tanstack/solid-router' +import { createServerFn } from '@tanstack/solid-start' + +const filePath = 'count.txt' + +async function readCount() { + return parseInt( + await fs.promises.readFile(filePath, 'utf-8').catch(() => '0'), + ) +} + +const getCount = createServerFn({ + method: 'GET', +}).handler(() => { + return readCount() +}) + +const updateCount = createServerFn({ method: 'POST' }) + .inputValidator((d: number) => d) + .handler(async ({ data }) => { + const count = await readCount() + await fs.promises.writeFile(filePath, `${count + data}`) + }) + +export const Route = createFileRoute('/')({ + component: Home, + loader: async () => await getCount(), +}) + +function Home() { + const router = useRouter() + const state = Route.useLoaderData() + + return ( + + ) +} +``` + +That's it! 🤯 You've now set up a TanStack Start project and written your first route. 🎉 + +You can now run `npm run dev` to start your server and navigate to `http://localhost:3000` to see your route in action. + +You want to deploy your application? Check out the [hosting guide](../hosting.md). diff --git a/docs/start/framework/solid/client-entry-point.md b/docs/start/framework/solid/client-entry-point.md index 41ff8a3abd5..e35589ebc20 100644 --- a/docs/start/framework/solid/client-entry-point.md +++ b/docs/start/framework/solid/client-entry-point.md @@ -1,5 +1,62 @@ --- -ref: docs/start/framework/react/client-entry-point.md -replace: - { '@tanstack/react-start': '@tanstack/solid-start', 'React': 'SolidJS' } +id: client-entry-point +title: Client Entry Point --- + +# Client Entry Point + +> [!NOTE] +> The client entry point is **optional** out of the box. If not provided, TanStack Start will automatically handle the client entry point for you using the below as a default. + +Getting our html to the client is only half the battle. Once there, we need to hydrate our client-side JavaScript once the route resolves to the client. We do this by hydrating the root of our application with the `StartClient` component: + +```tsx +// src/client.tsx +import { hydrate } from 'solid-js/web' +import { StartClient } from '@tanstack/solid-start/client' + +hydrate(() => , document.body) +``` + +This enables us to kick off client-side routing once the user's initial server request has fulfilled. + +## Error Handling + +You can wrap your client entry point with error boundaries to handle client-side errors gracefully: + +```tsx +// src/client.tsx +import { StartClient } from '@tanstack/solid-start/client' +import { hydrate } from 'solid-js/web' +import { ErrorBoundary } from './components/ErrorBoundary' + +hydrate( + () => ( + + + + ), + document.body, +) +``` + +## Development vs Production + +You may want different behavior in development vs production: + +```tsx +// src/client.tsx +import { StartClient } from '@tanstack/solid-start/client' +import { hydrate } from 'solid-js/web' + +const App = ( + <> + {import.meta.env.DEV &&
Development Mode
} + + +) + +hydrate(() => , document.body) +``` + +The client entry point gives you full control over how your application initializes on the client side while working seamlessly with TanStack Start's server-side rendering. diff --git a/docs/start/framework/solid/code-execution-patterns.md b/docs/start/framework/solid/code-execution-patterns.md index 2db5979f75a..deedc933059 100644 --- a/docs/start/framework/solid/code-execution-patterns.md +++ b/docs/start/framework/solid/code-execution-patterns.md @@ -1,5 +1,146 @@ --- -ref: docs/start/framework/react/code-execution-patterns.md -replace: - { '@tanstack/react-start': '@tanstack/solid-start', 'React': 'SolidJS' } +id: code-execution-patterns +title: Code Execution Patterns --- + +This guide covers patterns for controlling where code runs in your TanStack Start application - server-only, client-only, or isomorphic (both environments). For foundational concepts, see the [Execution Model](../execution-model.md) guide. + +## Quick Start + +Set up execution boundaries in your TanStack Start application: + +```tsx +import { + createServerFn, + createServerOnlyFn, + createClientOnlyFn, + createIsomorphicFn, +} from '@tanstack/solid-start' + +// Server function (RPC call) +const getUsers = createServerFn().handler(async () => { + return await db.users.findMany() +}) + +// Server-only utility (crashes on client) +const getSecret = createServerOnlyFn(() => process.env.API_SECRET) + +// Client-only utility (crashes on server) +const saveToStorage = createClientOnlyFn((data: any) => { + localStorage.setItem('data', JSON.stringify(data)) +}) + +// Different implementations per environment +const logger = createIsomorphicFn() + .server((msg) => console.log(`[SERVER]: ${msg}`)) + .client((msg) => console.log(`[CLIENT]: ${msg}`)) +``` + +## Implementation Patterns + +### Progressive Enhancement + +```tsx +// Component works without JS, enhanced with JS +function SearchForm() { + const [query, setQuery] = createSignal('') + + return ( +
+ setQuery(e.target.value)} + /> + Search}> + search(query())} /> + +
+ ) +} +``` + +### Environment-Aware Storage + +```tsx +const storage = createIsomorphicFn() + .server((key: string) => { + // Server: File-based cache + const fs = require('node:fs') + return JSON.parse(fs.readFileSync('.cache', 'utf-8'))[key] + }) + .client((key: string) => { + // Client: localStorage + return JSON.parse(localStorage.getItem(key) || 'null') + }) +``` + +## Common Problems + +### Environment Variable Exposure + +```tsx +// ❌ Exposes to client bundle +const apiKey = process.env.SECRET_KEY + +// ✅ Server-only access +const apiKey = createServerOnlyFn(() => process.env.SECRET_KEY) +``` + +### Incorrect Loader Assumptions + +```tsx +// ❌ Assuming loader is server-only +export const Route = createFileRoute('/users')({ + loader: () => { + // This runs on BOTH server and client! + const secret = process.env.SECRET // Exposed to client + return fetch(`/api/users?key=${secret}`) + }, +}) + +// ✅ Use server function for server-only operations +const getUsersSecurely = createServerFn().handler(() => { + const secret = process.env.SECRET // Server-only + return fetch(`/api/users?key=${secret}`) +}) + +export const Route = createFileRoute('/users')({ + loader: () => getUsersSecurely(), // Isomorphic call to server function +}) +``` + +### Hydration Mismatches + +```tsx +// ❌ Different content server vs client +function CurrentTime() { + return
{new Date().toLocaleString()}
+} + +// ✅ Consistent rendering +function CurrentTime() { + const [time, setTime] = createSignal() + + createEffect(() => { + setTime(new Date().toLocaleString()) + }) + + return
{time() || 'Loading...'}
+} +``` + +## Production Checklist + +- [ ] **Bundle Analysis**: Verify server-only code isn't in client bundle +- [ ] **Environment Variables**: Ensure secrets use `createServerOnlyFn()` or `createServerFn()` +- [ ] **Loader Logic**: Remember loaders are isomorphic, not server-only +- [ ] **ClientOnly Fallbacks**: Provide appropriate fallbacks to prevent layout shift +- [ ] **Error Boundaries**: Handle server/client execution errors gracefully + +## Related Resources + +- [Execution Model](../execution-model.md) - Core concepts and architectural patterns +- [Server Functions](../server-functions.md) - Deep dive into server function patterns +- [Environment Variables](../environment-variables.md) - Secure environment variable handling +- [Middleware](../middleware.md) - Server function middleware patterns diff --git a/docs/start/framework/solid/environment-variables.md b/docs/start/framework/solid/environment-variables.md index 743c7b56d63..37ef1ed4af2 100644 --- a/docs/start/framework/solid/environment-variables.md +++ b/docs/start/framework/solid/environment-variables.md @@ -1,5 +1,473 @@ --- -ref: docs/start/framework/react/environment-variables.md -replace: - { '@tanstack/react-start': '@tanstack/solid-start', 'React': 'SolidJS' } +id: environment-variables +title: Environment Variables --- + +Learn how to securely configure and use environment variables in your TanStack Start application across different contexts (server functions, client code, and build processes). + +## Quick Start + +TanStack Start automatically loads `.env` files and makes variables available in both server and client contexts with proper security boundaries. + +```bash +# .env +DATABASE_URL=postgresql://user:pass@localhost:5432/mydb +VITE_APP_NAME=My TanStack Start App +``` + +```typescript +// Server function - can access any environment variable +const getUser = createServerFn().handler(async () => { + const db = await connect(process.env.DATABASE_URL) // ✅ Server-only + return db.user.findFirst() +}) + +// Client component - only VITE_ prefixed variables +export function AppHeader() { + return

{import.meta.env.VITE_APP_NAME}

// ✅ Client-safe +} +``` + +## Environment Variable Contexts + +### Server-Side Context (Server Functions & API Routes) + +Server functions can access **any** environment variable using `process.env`: + +```typescript +import { createServerFn } from '@tanstack/solid-start' + +// Database connection (server-only) +const connectToDatabase = createServerFn().handler(async () => { + const connectionString = process.env.DATABASE_URL // No prefix needed + const apiKey = process.env.EXTERNAL_API_SECRET // Stays on server + + // These variables are never exposed to the client + return await database.connect(connectionString) +}) + +// Authentication (server-only) +const authenticateUser = createServerFn() + .inputValidator(z.object({ token: z.string() })) + .handler(async ({ data }) => { + const jwtSecret = process.env.JWT_SECRET // Server-only + return jwt.verify(data.token, jwtSecret) + }) +``` + +### Client-Side Context (Components & Client Code) + +Client code can only access variables with the `VITE_` prefix: + +```typescript +// Client configuration +export function ApiProvider(props) { + const apiUrl = import.meta.env.VITE_API_URL // ✅ Public + const apiKey = import.meta.env.VITE_PUBLIC_KEY // ✅ Public + + // This would be undefined (security feature): + // const secret = import.meta.env.DATABASE_URL // ❌ Undefined + + return ( + + {props.children} + + ) +} + +// Feature flags +export function FeatureGatedComponent() { + const enableNewFeature = import.meta.env.VITE_ENABLE_NEW_FEATURE === 'true' + + if (!enableNewFeature) return null + + return +} +``` + +## Environment File Setup + +### File Hierarchy (Loaded in Order) + +TanStack Start automatically loads environment files in this order: + +``` +.env.local # Local overrides (add to .gitignore) +.env.production # Production-specific variables +.env.development # Development-specific variables +.env # Default variables (commit to git) +``` + +### Example Setup + +**.env** (committed to repository): + +```bash +# Public configuration +VITE_APP_NAME=My TanStack Start App +VITE_API_URL=https://api.example.com +VITE_SENTRY_DSN=https://... + +# Server configuration templates +DATABASE_URL=postgresql://localhost:5432/myapp_dev +REDIS_URL=redis://localhost:6379 +``` + +**.env.local** (add to .gitignore): + +```bash +# Override for local development +DATABASE_URL=postgresql://user:password@localhost:5432/myapp_local +STRIPE_SECRET_KEY=sk_test_... +JWT_SECRET=your-local-secret +``` + +**.env.production**: + +```bash +# Production overrides +VITE_API_URL=https://api.myapp.com +DATABASE_POOL_SIZE=20 +``` + +## Common Patterns + +### Database Configuration + +```typescript +// src/lib/database.ts +import { createServerFn } from '@tanstack/solid-start' + +const getDatabaseConnection = createServerFn().handler(async () => { + const config = { + url: process.env.DATABASE_URL, + maxConnections: parseInt(process.env.DB_MAX_CONNECTIONS || '10'), + ssl: process.env.NODE_ENV === 'production', + } + + return createConnection(config) +}) +``` + +### Authentication Provider Setup + +```typescript +// src/lib/auth.ts (Server) +export const authConfig = { + secret: process.env.AUTH_SECRET, + providers: { + auth0: { + domain: process.env.AUTH0_DOMAIN, + clientId: process.env.AUTH0_CLIENT_ID, + clientSecret: process.env.AUTH0_CLIENT_SECRET, // Server-only + } + } +} + +// src/components/AuthProvider.tsx (Client) +export function AuthProvider(props) { + return ( + + {props.children} + + ) +} +``` + +### External API Integration + +```typescript +// src/lib/external-api.ts +import { createServerFn } from '@tanstack/solid-start' + +// Server-side API calls (can use secret keys) +const fetchUserData = createServerFn() + .inputValidator(z.object({ userId: z.string() })) + .handler(async ({ data }) => { + const response = await fetch( + `${process.env.EXTERNAL_API_URL}/users/${data.userId}`, + { + headers: { + Authorization: `Bearer ${process.env.EXTERNAL_API_SECRET}`, + 'Content-Type': 'application/json', + }, + }, + ) + + return response.json() + }) + +// Client-side API calls (public endpoints only) +export function usePublicData() { + const apiUrl = import.meta.env.VITE_PUBLIC_API_URL + + return useQuery({ + queryKey: ['public-data'], + queryFn: () => fetch(`${apiUrl}/public/stats`).then((r) => r.json()), + }) +} +``` + +### Feature Flags and Configuration + +```typescript +// src/config/features.ts +export const featureFlags = { + enableNewDashboard: import.meta.env.VITE_ENABLE_NEW_DASHBOARD === 'true', + enableAnalytics: import.meta.env.VITE_ENABLE_ANALYTICS === 'true', + debugMode: import.meta.env.VITE_DEBUG_MODE === 'true', +} + +// Usage in components +export function Dashboard() { + if (featureFlags.enableNewDashboard) { + return + } + + return +} +``` + +## Type Safety + +### TypeScript Declarations + +Create `src/env.d.ts` to add type safety: + +```typescript +/// + +interface ImportMetaEnv { + // Client-side environment variables + readonly VITE_APP_NAME: string + readonly VITE_API_URL: string + readonly VITE_AUTH0_DOMAIN: string + readonly VITE_AUTH0_CLIENT_ID: string + readonly VITE_SENTRY_DSN?: string + readonly VITE_ENABLE_NEW_DASHBOARD?: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} + +// Server-side environment variables +declare global { + namespace NodeJS { + interface ProcessEnv { + readonly DATABASE_URL: string + readonly REDIS_URL: string + readonly JWT_SECRET: string + readonly AUTH0_CLIENT_SECRET: string + readonly STRIPE_SECRET_KEY: string + readonly NODE_ENV: 'development' | 'production' | 'test' + } + } +} + +export {} +``` + +### Runtime Validation + +Use Zod for runtime validation of environment variables: + +```typescript +// src/config/env.ts +import { z } from 'zod' + +const envSchema = z.object({ + DATABASE_URL: z.string().url(), + JWT_SECRET: z.string().min(32), + NODE_ENV: z.enum(['development', 'production', 'test']), +}) + +const clientEnvSchema = z.object({ + VITE_APP_NAME: z.string(), + VITE_API_URL: z.string().url(), + VITE_AUTH0_DOMAIN: z.string(), + VITE_AUTH0_CLIENT_ID: z.string(), +}) + +// Validate server environment +export const serverEnv = envSchema.parse(process.env) + +// Validate client environment +export const clientEnv = clientEnvSchema.parse(import.meta.env) +``` + +## Security Best Practices + +### 1. Never Expose Secrets to Client + +```typescript +// ❌ WRONG - Secret exposed to client bundle +const config = { + apiKey: import.meta.env.VITE_SECRET_API_KEY, // This will be in your JS bundle! +} + +// ✅ CORRECT - Keep secrets on server +const getApiData = createServerFn().handler(async () => { + const response = await fetch(apiUrl, { + headers: { Authorization: `Bearer ${process.env.SECRET_API_KEY}` }, + }) + return response.json() +}) +``` + +### 2. Use Appropriate Prefixes + +```bash +# ✅ Server-only (no prefix) +DATABASE_URL=postgresql://... +JWT_SECRET=super-secret-key +STRIPE_SECRET_KEY=sk_live_... + +# ✅ Client-safe (VITE_ prefix) +VITE_APP_NAME=My App +VITE_API_URL=https://api.example.com +VITE_SENTRY_DSN=https://... +``` + +### 3. Validate Required Variables + +```typescript +// src/config/validation.ts +const requiredServerEnv = ['DATABASE_URL', 'JWT_SECRET'] as const + +const requiredClientEnv = ['VITE_APP_NAME', 'VITE_API_URL'] as const + +// Validate on server startup +for (const key of requiredServerEnv) { + if (!process.env[key]) { + throw new Error(`Missing required environment variable: ${key}`) + } +} + +// Validate client environment at build time +for (const key of requiredClientEnv) { + if (!import.meta.env[key]) { + throw new Error(`Missing required environment variable: ${key}`) + } +} +``` + +## Production Checklist + +- [ ] All sensitive variables are server-only (no `VITE_` prefix) +- [ ] Client variables use `VITE_` prefix +- [ ] `.env.local` is in `.gitignore` +- [ ] Production environment variables are configured on hosting platform +- [ ] Required environment variables are validated at startup +- [ ] No hardcoded secrets in source code +- [ ] Database URLs use connection pooling in production +- [ ] API keys are rotated regularly + +## Common Problems + +### Environment Variable is Undefined + +**Problem**: `import.meta.env.MY_VARIABLE` returns `undefined` + +**Solutions**: + +1. **Add correct prefix**: Use `VITE_` prefix (e.g. `VITE_MY_VARIABLE`) +2. **Restart development server** after adding new variables +3. **Check file location**: `.env` file must be in project root +4. **Verify bundler configuration**: Ensure variables are properly injected + +**Example**: + +```bash +# ❌ Won't work in client code +API_KEY=abc123 + +# ✅ Works in client code +VITE_API_KEY=abc123 + +# ❌ Won't bundle the variable (assuming it is not set in the environment of the build) +npm run build + +# ✅ Works in client code and will bundle the variable for production +VITE_API_KEY=abc123 npm run build +``` + +### Runtime Client Environment Variables in Production + +**Problem**: If `VITE_` variables are replaced at bundle time only, how to make runtime variables available on the client? + +**Solutions**: + +Pass variables from the server down to the client: + +```tsx +const getRuntimeVar = createServerFn({ method: 'GET' }).handler(() => { + return process.env.MY_RUNTIME_VAR // notice `process.env` on the server, and no `VITE_` prefix +}) + +export const Route = createFileRoute('/')({ + loader: async () => { + const foo = await getRuntimeVar() + return { foo } + }, + component: RouteComponent, +}) + +function RouteComponent() { + const { foo } = Route.useLoaderData() + // ... use your variable however you want +} +``` + +### Variable Not Updating + +**Problem**: Environment variable changes aren't reflected + +**Solutions**: + +1. Restart development server +2. Check if you're modifying the correct `.env` file +3. Verify file hierarchy (`.env.local` overrides `.env`) + +### TypeScript Errors + +**Problem**: `Property 'VITE_MY_VAR' does not exist on type 'ImportMetaEnv'` + +**Solution**: Add to `src/env.d.ts`: + +```typescript +interface ImportMetaEnv { + readonly VITE_MY_VAR: string +} +``` + +### Security: Secret Exposed to Client + +**Problem**: Sensitive data appearing in client bundle + +**Solutions**: + +1. Remove `VITE_` prefix from sensitive variables +2. Move sensitive operations to server functions +3. Use build tools to verify no secrets in client bundle + +### Build Errors in Production + +**Problem**: Missing environment variables in production build + +**Solutions**: + +1. Configure variables on hosting platform +2. Validate required variables at build time +3. Use deployment-specific `.env` files + +## Related Resources + +- [Code Execution Patterns](../code-execution-patterns.md) - Learn about server vs client code execution +- [Server Functions](../server-functions.md) - Learn more about server-side code +- [Hosting](../hosting.md) - Platform-specific environment variable configuration +- [Vite Environment Variables](https://vitejs.dev/guide/env-and-mode.html) - Official Vite documentation diff --git a/docs/start/framework/solid/execution-model.md b/docs/start/framework/solid/execution-model.md index c9e717a7e42..98d9c2b7cad 100644 --- a/docs/start/framework/solid/execution-model.md +++ b/docs/start/framework/solid/execution-model.md @@ -1,5 +1,307 @@ --- -ref: docs/start/framework/react/execution-model.md -replace: - { '@tanstack/react-start': '@tanstack/solid-start', 'React': 'SolidJS' } +id: execution-model +title: Execution Model --- + +Understanding where code runs is fundamental to building TanStack Start applications. This guide explains TanStack Start's execution model and how to control where your code executes. + +## Core Principle: Isomorphic by Default + +**All code in TanStack Start is isomorphic by default** - it runs and is included in both server and client bundles unless explicitly constrained. + +```tsx +// ✅ This runs on BOTH server and client +function formatPrice(price: number) { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'USD', + }).format(price) +} + +// ✅ Route loaders are ISOMORPHIC +export const Route = createFileRoute('/products')({ + loader: async () => { + // This runs on server during SSR AND on client during navigation + const response = await fetch('/api/products') + return response.json() + }, +}) +``` + +> **Critical Understanding**: Route `loader`s are isomorphic - they run on both server and client, not just the server. + +## The Execution Boundary + +TanStack Start applications run in two environments: + +### Server Environment + +- **Node.js runtime** with access to file system, databases, environment variables +- **During SSR** - Initial page renders on server +- **API requests** - Server functions execute server-side +- **Build time** - Static generation and pre-rendering + +### Client Environment + +- **Browser runtime** with access to DOM, localStorage, user interactions +- **After hydration** - Client takes over after initial server render +- **Navigation** - Route loaders run client-side during navigation +- **User interactions** - Event handlers, form submissions, etc. + +## Execution Control APIs + +### Server-Only Execution + +| API | Use Case | Client Behavior | +| ------------------------ | ------------------------- | ------------------------- | +| `createServerFn()` | RPC calls, data mutations | Network request to server | +| `createServerOnlyFn(fn)` | Utility functions | Throws error | + +```tsx +import { createServerFn, createServerOnlyFn } from '@tanstack/solid-start' + +// RPC: Server execution, callable from client +const updateUser = createServerFn({ method: 'POST' }) + .inputValidator((data: UserData) => data) + .handler(async ({ data }) => { + // Only runs on server, but client can call it + return await db.users.update(data) + }) + +// Utility: Server-only, client crashes if called +const getEnvVar = createServerOnlyFn(() => process.env.DATABASE_URL) +``` + +### Client-Only Execution + +| API | Use Case | Server Behavior | +| ------------------------ | ------------------------------- | ---------------- | +| `createClientOnlyFn(fn)` | Browser utilities | Throws error | +| `` | Components needing browser APIs | Renders fallback | + +```tsx +import { createClientOnlyFn } from '@tanstack/solid-start' +import { ClientOnly } from '@tanstack/solid-router' + +// Utility: Client-only, server crashes if called +const saveToStorage = createClientOnlyFn((key: string, value: any) => { + localStorage.setItem(key, JSON.stringify(value)) +}) + +// Component: Only renders children after hydration +function Analytics() { + return ( + + + + ) +} +``` + +### Environment-Specific Implementations + +```tsx +import { createIsomorphicFn } from '@tanstack/solid-start' + +// Different implementation per environment +const getDeviceInfo = createIsomorphicFn() + .server(() => ({ type: 'server', platform: process.platform })) + .client(() => ({ type: 'client', userAgent: navigator.userAgent })) +``` + +## Architectural Patterns + +### Progressive Enhancement + +Build components that work without JavaScript and enhance with client-side functionality: + +```tsx +function SearchForm() { + const [query, setQuery] = createSignal('') + + return ( +
+ setQuery(e.target.value)} + /> + Search}> + search(query())} /> + +
+ ) +} +``` + +### Environment-Aware Storage + +```tsx +const storage = createIsomorphicFn() + .server((key: string) => { + // Server: File-based cache + const fs = require('node:fs') + return JSON.parse(fs.readFileSync('.cache', 'utf-8'))[key] + }) + .client((key: string) => { + // Client: localStorage + return JSON.parse(localStorage.getItem(key) || 'null') + }) +``` + +### RPC vs Direct Function Calls + +Understanding when to use server functions vs server-only functions: + +```tsx +// createServerFn: RPC pattern - server execution, client callable +const fetchUser = createServerFn().handler(async () => await db.users.find()) + +// Usage from client component: +const user = await fetchUser() // ✅ Network request + +// createServerOnlyFn: Crashes if called from client +const getSecret = createServerOnlyFn(() => process.env.SECRET) + +// Usage from client: +const secret = getSecret() // ❌ Throws error +``` + +## Common Anti-Patterns + +### Environment Variable Exposure + +```tsx +// ❌ Exposes to client bundle +const apiKey = process.env.SECRET_KEY + +// ✅ Server-only access +const apiKey = createServerOnlyFn(() => process.env.SECRET_KEY) +``` + +### Incorrect Loader Assumptions + +```tsx +// ❌ Assuming loader is server-only +export const Route = createFileRoute('/users')({ + loader: () => { + // This runs on BOTH server and client! + const secret = process.env.SECRET // Exposed to client + return fetch(`/api/users?key=${secret}`) + }, +}) + +// ✅ Use server function for server-only operations +const getUsersSecurely = createServerFn().handler(() => { + const secret = process.env.SECRET // Server-only + return fetch(`/api/users?key=${secret}`) +}) + +export const Route = createFileRoute('/users')({ + loader: () => getUsersSecurely(), // Isomorphic call to server function +}) +``` + +### Hydration Mismatches + +```tsx +// ❌ Different content server vs client +function CurrentTime() { + return
{new Date().toLocaleString()}
+} + +// ✅ Consistent rendering +function CurrentTime() { + const [time, setTime] = createSignal() + + createEffect(() => { + setTime(new Date().toLocaleString()) + }) + + return
{time() || 'Loading...'}
+} +``` + +## Manual vs API-Driven Environment Detection + +```tsx +// Manual: You handle the logic +function logMessage(msg: string) { + if (typeof window === 'undefined') { + console.log(`[SERVER]: ${msg}`) + } else { + console.log(`[CLIENT]: ${msg}`) + } +} + +// API: Framework handles it +const logMessage = createIsomorphicFn() + .server((msg) => console.log(`[SERVER]: ${msg}`)) + .client((msg) => console.log(`[CLIENT]: ${msg}`)) +``` + +## Architecture Decision Framework + +**Choose Server-Only when:** + +- Accessing sensitive data (environment variables, secrets) +- File system operations +- Database connections +- External API keys + +**Choose Client-Only when:** + +- DOM manipulation +- Browser APIs (localStorage, geolocation) +- User interaction handling +- Analytics/tracking + +**Choose Isomorphic when:** + +- Data formatting/transformation +- Business logic +- Shared utilities +- Route loaders (they're isomorphic by nature) + +## Security Considerations + +### Bundle Analysis + +Always verify server-only code isn't included in client bundles: + +```bash +# Analyze client bundle +npm run build +# Check dist/client for any server-only imports +``` + +### Environment Variable Strategy + +- **Client-exposed**: Use `VITE_` prefix for client-accessible variables +- **Server-only**: Access via `createServerOnlyFn()` or `createServerFn()` +- **Never expose**: Database URLs, API keys, secrets + +### Error Boundaries + +Handle server/client execution errors gracefully: + +```tsx +function ErrorBoundary(props) { + return ( + Something went wrong} + onError={(error) => { + if (typeof window === 'undefined') { + console.error('[SERVER ERROR]:', error) + } else { + console.error('[CLIENT ERROR]:', error) + } + }} + > + {props.children} + + ) +} +``` + +Understanding TanStack Start's execution model is crucial for building secure, performant, and maintainable applications. The isomorphic-by-default approach provides flexibility while the execution control APIs give you precise control when needed. diff --git a/docs/start/framework/solid/getting-started.md b/docs/start/framework/solid/getting-started.md index ecfec257702..b9be5be3b2a 100644 --- a/docs/start/framework/solid/getting-started.md +++ b/docs/start/framework/solid/getting-started.md @@ -1,9 +1,17 @@ --- -ref: docs/start/framework/react/getting-started.md -replace: - { - '@tanstack/react-start': '@tanstack/solid-start', - 'React': 'SolidJS', - 'react-router': 'solid-router', - } +id: getting-started +title: Getting Started --- + +## Start a new project from scratch + +Choose one of the following options to start building a _new_ TanStack Start project: + +- [TanStack Start CLI] - Just run `npm create @tanstack/start@latest`. Local, fast, and optionally customizable +- [TanStack Builder](#) (coming soon!) - A visual interface to configure new TanStack projects with a few clicks +- [Quick Start Examples](../quick-start) Download or clone one of our official examples +- [Build a project from scratch](../build-from-scratch) - A guide to building a TanStack Start project line-by-line, file-by-file. + +## Next Steps + +Unless you chose to build a project from scratch, you can now move on to the [Routing](../routing) guide to learn how to use TanStack Start! diff --git a/docs/start/framework/solid/hosting.md b/docs/start/framework/solid/hosting.md index ce646d636e4..ffeaa0b9b20 100644 --- a/docs/start/framework/solid/hosting.md +++ b/docs/start/framework/solid/hosting.md @@ -1,5 +1,257 @@ --- -ref: docs/start/framework/react/hosting.md -replace: - { '@tanstack/react-start': '@tanstack/solid-start', 'React': 'SolidJS' } +id: hosting +title: Hosting --- + +Hosting is the process of deploying your application to the internet so that users can access it. This is a critical part of any web development project, ensuring your application is available to the world. TanStack Start is built on Vite, a powerful dev/build platform that allows us to make it possible to deploy your application to any hosting provider. + +## What should I use? + +TanStack Start is **designed to work with any hosting provider**, so if you already have a hosting provider in mind, you can deploy your application there using the full-stack APIs provided by TanStack Start. + +However, since hosting is one of the most crucial aspects of your application's performance, reliability, and scalability, we recommend using one of our **Official Hosting Partners**: [Cloudflare](https://www.cloudflare.com?utm_source=tanstack) or [Netlify](https://www.netlify.com?utm_source=tanstack). + +## Deployment + +> [!WARNING] +> The page is still a work in progress. We'll keep updating this page with guides on deployment to different hosting providers soon! + +Once you've chosen a deployment target, you can follow the deployment guidelines below to deploy your TanStack Start application to the hosting provider of your choice: + +- [`cloudflare-workers`](#cloudflare-workers): Deploy to Cloudflare Workers +- [`netlify`](#netlify): Deploy to Netlify +- [`nitro`](#nitro): Deploy using Nitro +- [`vercel`](#vercel): Deploy to Vercel +- [`railway`](#nodejs--railway--docker): Deploy to Railway +- [`node-server`](#nodejs--railway--docker): Deploy to a Node.js server +- [`bun`](#bun): Deploy to a Bun server +- ... and more to come! + +### Cloudflare Workers ⭐ _Official Partner_ + + + + + + Cloudflare logo + + + +### Cloudflare Workers + +When deploying to Cloudflare Workers, you'll need to complete a few extra steps before your users can start using your app. + +1. Install `@cloudflare/vite-plugin` + +```bash +pnpm install @cloudflare/vite-plugin -D +``` + +2. Update `vite.config.ts` + +Add the cloudflare plugin to your `vite.config.ts` file. + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import { cloudflare } from '@cloudflare/vite-plugin' +import viteSolid from 'vite-plugin-solid' + +export default defineConfig({ + plugins: [ + cloudflare({ viteEnvironment: { name: 'ssr' } }), + tanstackStart(), + viteSolid({ ssr: true }), + ], +}) +``` + +3. Install `wrangler` + +```bash +pnpm add wrangler -D +``` + +4. Add a `wrangler.json` config file + +```json +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "tanstack-start-app", + "compatibility_date": "2025-09-02", + "compatibility_flags": ["nodejs_compat"], + "main": "@tanstack/solid-start/server-entry", + "vars": { + "MY_VAR": "Hello from Cloudflare" + } +} +``` + +5. Modify package.json script + +```json +{ + "scripts": { + "dev": "vite dev", + "build": "vite build && tsc --noEmit", + "start": "node .output/server/index.mjs", + // ============ 👇 add this line ============ + "deploy": "wrangler deploy" + } +} +``` + +6. Build and deploy + +```bash +pnpm run build && pnpm run deploy +``` + +Deploy your application to Cloudflare Workers using their one-click deployment process, and you're ready to go! + +### Netlify ⭐ _Official Partner_ + + + + + + Netlify logo + + + +### Netlify + +Install and add the [`@netlify/vite-plugin-tanstack-start`](https://www.npmjs.com/package/@netlify/vite-plugin-tanstack-start) plugin, which configures your build for Netlify deployment and provides full Netlify production platform emulation in local dev. + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import netlify from '@netlify/vite-plugin-tanstack-start' +import viteSolid from 'vite-plugin-solid' + +export default defineConfig({ + plugins: [tanstackStart(), netlify(), viteSolid({ ssr: true })], +}) +``` + +Add a `netlify.toml` file to your project root: + +```toml +[build] + command = "vite build" + publish = "dist/client" +``` + +Deploy your application using their one-click deployment process, and you're ready to go! + +### Nitro + +[Nitro](https://nitro.build/) is an abstraction layer that allows you to deploy TanStack Start applications to [a wide range of providers](https://nitro.build/deploy). + +**⚠️ During TanStack Start 1.0 release candidate phase, we currently recommend using:** + +- [@tanstack/nitro-v2-vite-plugin (Temporary Compatibility Plugin)](https://www.npmjs.com/package/@tanstack/nitro-v2-vite-plugin) - A temporary compatibility plugin for using Nitro v2 as the underlying build tool for TanStack Start. +- [Nitro v3's Vite Plugin (BETA)](https://www.npmjs.com/package/nitro-nightly) - A **BETA** plugin for officially using Nitro v3 as the underlying build tool for TanStack Start. + +#### Using Nitro v2 + +**⚠️ `@tanstack/nitro-v2-vite-plugin` is a temporary compatibility plugin for using Nitro v2 as the underlying build tool for TanStack Start. Use this plugin if you experience issues with the Nitro v3 plugin. It does not support all of Nitro v3's features and is limited in its dev server capabilities, but should work as a safe fallback, even for production deployments for those who were using TanStack Start's alpha/beta versions.** + +```tsx +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import { defineConfig } from 'vite' +import viteSolid from 'vite-plugin-solid' +import { nitroV2Plugin } from '@tanstack/nitro-v2-vite-plugin' + +export default defineConfig({ + plugins: [ + tanstackStart(), + nitroV2Plugin(/* + // nitro config goes here, e.g. + { preset: 'node-server' } + */), + viteSolid({ ssr: true }), + ], +}) +``` + +#### Using Nitro v3 (BETA) + +**⚠️ The `nitro` vite plugin is an official **BETA** plugin from the Nitro team for using Nitro v3 as the underlying build tool for TanStack Start. It is still in development and is receiving regular updates.** + +This package needs to be installed as follows: + +``` + "nitro": "npm:nitro-nightly", +``` + +```tsx +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import { defineConfig } from 'vite' +import viteSolid from 'vite-plugin-solid' +import { nitro } from 'nitro/vite' + +export default defineConfig({ + plugins: [ + tanstackStart(), + nitro(/* + // nitro config goes here, e.g. + { config: { preset: 'node-server' } } + */) + viteSolid({ssr: true}), + ], +}) +``` + +### Vercel + +Follow the [`Nitro`](#nitro) deployment instructions. +Deploy your application to Vercel using their one-click deployment process, and you're ready to go! + +### Node.js / Railway / Docker + +Follow the [`Nitro`](#nitro) deployment instructions. Use the `node` command to start your application from the server from the build output files. + +Ensure `build` and `start` npm scripts are present in your `package.json` file: + +```json + "build": "vite build", + "start": "node .output/server/index.mjs" +``` + +Then you can run the following command to build your application: + +```sh +npm run build +``` + +You can start your application by running: + +```sh +npm run start +``` + +### Bun + +Follow the [`Nitro`](#nitro) deployment instructions. +Depending on how you invoke the build, you might need to set the `'bun'` preset in the Nitro configuration: + +```ts +// vite.config.ts +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import { defineConfig } from 'vite' +import viteSolid from 'vite-plugin-solid' +import { nitroV2Plugin } from '@tanstack/nitro-v2-vite-plugin' +// alternatively: import { nitro } from 'nitro/vite' + +export default defineConfig({ + plugins: [ + tanstackStart(), + nitroV2Plugin({ preset: 'bun' }) + // alternatively: nitro( { config: { preset: 'bun' }} ), + viteSolid({ssr: true}), + ], +}) +``` diff --git a/docs/start/framework/solid/observability.md b/docs/start/framework/solid/observability.md index 5d7eebdf456..73edf5929a5 100644 --- a/docs/start/framework/solid/observability.md +++ b/docs/start/framework/solid/observability.md @@ -1,5 +1,661 @@ --- -ref: docs/start/framework/react/observability.md -replace: - { '@tanstack/react-start': '@tanstack/solid-start', 'React': 'SolidJS' } +id: observability +title: Observability --- + +Observability is a critical aspect of modern web development, enabling you to monitor, trace, and debug your application's performance and errors. TanStack Start provides built-in patterns for observability and integrates seamlessly with external tools to give you comprehensive insights into your application. + +## Partner Solution: Sentry + + + + + + Sentry logo + + + +For comprehensive observability, we recommend [Sentry](https://sentry.io?utm_source=tanstack) - our trusted partner for error tracking and performance monitoring. Sentry provides: + +- **Real-time Error Tracking** - Catch and debug errors across your entire stack +- **Performance Monitoring** - Track slow transactions and optimize bottlenecks +- **Release Health** - Monitor deployments and track error rates over time +- **User Impact Analysis** - Understand how errors affect your users +- **TanStack Start Integration** - Works seamlessly with server functions and client code + +**Quick Setup:** + +```tsx +// Client-side (app.tsx) +import * as Sentry from '@sentry/solid' + +Sentry.init({ + dsn: import.meta.env.VITE_SENTRY_DSN, + environment: import.meta.env.NODE_ENV, +}) + +// Server functions +import * as Sentry from '@sentry/node' + +const serverFn = createServerFn().handler(async () => { + try { + return await riskyOperation() + } catch (error) { + Sentry.captureException(error) + throw error + } +}) +``` + +[Get started with Sentry →](https://sentry.io/signup?utm_source=tanstack) + +## Built-in Observability Patterns + +TanStack Start's architecture provides several opportunities for built-in observability without external dependencies: + +### Server Function Logging + +Add logging to your server functions to track execution, performance, and errors: + +```tsx +import { createServerFn } from '@tanstack/solid-start' + +const getUser = createServerFn({ method: 'GET' }) + .inputValidator((id: string) => id) + .handler(async ({ data: id }) => { + const startTime = Date.now() + + try { + console.log(`[SERVER] Fetching user ${id}`) + + const user = await db.users.findUnique({ where: { id } }) + + if (!user) { + console.log(`[SERVER] User ${id} not found`) + throw new Error('User not found') + } + + const duration = Date.now() - startTime + console.log(`[SERVER] User ${id} fetched in ${duration}ms`) + + return user + } catch (error) { + const duration = Date.now() - startTime + console.error( + `[SERVER] Error fetching user ${id} after ${duration}ms:`, + error, + ) + throw error + } + }) +``` + +### Request/Response Middleware + +Create middleware to log all requests and responses: + +```tsx +import { createMiddleware } from '@tanstack/solid-start' + +const requestLogger = createMiddleware().handler(async ({ next }) => { + const startTime = Date.now() + const timestamp = new Date().toISOString() + + console.log(`[${timestamp}] ${request.method} ${request.url} - Starting`) + + try { + const response = await next() + const duration = Date.now() - startTime + + console.log( + `[${timestamp}] ${request.method} ${request.url} - ${response.status} (${duration}ms)`, + ) + + return response + } catch (error) { + const duration = Date.now() - startTime + console.error( + `[${timestamp}] ${request.method} ${request.url} - Error (${duration}ms):`, + error, + ) + throw error + } +}) + +// Apply to all server routes +export const Route = createFileRoute('/api/users')({ + server: { + middleware: [requestLogger], + handlers: { + GET: async () => { + return json({ users: await getUsers() }) + }, + }, + }, +}) +``` + +### Route Performance Monitoring + +Track route loading performance on both client and server: + +```tsx +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/dashboard')({ + loader: async ({ context }) => { + const startTime = Date.now() + + try { + const data = await loadDashboardData() + const duration = Date.now() - startTime + + // Log server-side performance + if (typeof window === 'undefined') { + console.log(`[SSR] Dashboard loaded in ${duration}ms`) + } + + return data + } catch (error) { + const duration = Date.now() - startTime + console.error(`[LOADER] Dashboard error after ${duration}ms:`, error) + throw error + } + }, + component: Dashboard, +}) + +function Dashboard() { + const data = Route.useLoaderData() + + // Track client-side render time + Solid.createEffect(() => { + const renderTime = performance.now() + console.log(`[CLIENT] Dashboard rendered in ${renderTime}ms`) + }) + + return
Dashboard content
+} +``` + +### Health Check Endpoints + +Create server routes for health monitoring: + +```tsx +// routes/health.ts +import { createFileRoute } from '@tanstack/solid-router' +import { json } from '@tanstack/solid-start' + +export const Route = createFileRoute('/health')({ + server: { + handlers: { + GET: async () => { + const checks = { + status: 'healthy', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + memory: process.memoryUsage(), + database: await checkDatabase(), + version: process.env.npm_package_version, + } + + return json(checks) + }, + }, + }, +}) + +async function checkDatabase() { + try { + await db.raw('SELECT 1') + return { status: 'connected', latency: 0 } + } catch (error) { + return { status: 'error', error: error.message } + } +} +``` + +### Error Boundaries + +Implement comprehensive error handling: + +```tsx +// Client-side error boundary +import { ErrorBoundary } from 'solid-error-boundary' + +function ErrorFallback({ error, resetErrorBoundary }: any) { + // Log client errors + console.error('[CLIENT ERROR]:', error) + + // Could also send to external service + // sendErrorToService(error) + + return ( +
+

Something went wrong

+ +
+ ) +} + +export function App() { + return ( + + + + ) +} + +// Server function error handling +const riskyOperation = createServerFn().handler(async () => { + try { + return await performOperation() + } catch (error) { + // Log server errors with context + console.error('[SERVER ERROR]:', { + error: error.message, + stack: error.stack, + timestamp: new Date().toISOString(), + // Add request context if available + }) + + // Return user-friendly error + throw new Error('Operation failed. Please try again.') + } +}) +``` + +### Performance Metrics Collection + +Collect and expose basic performance metrics: + +```tsx +// utils/metrics.ts +class MetricsCollector { + private metrics = new Map() + + recordTiming(name: string, duration: number) { + if (!this.metrics.has(name)) { + this.metrics.set(name, []) + } + this.metrics.get(name)!.push(duration) + } + + getStats(name: string) { + const timings = this.metrics.get(name) || [] + if (timings.length === 0) return null + + const sorted = timings.sort((a, b) => a - b) + return { + count: timings.length, + avg: timings.reduce((a, b) => a + b, 0) / timings.length, + p50: sorted[Math.floor(sorted.length * 0.5)], + p95: sorted[Math.floor(sorted.length * 0.95)], + min: sorted[0], + max: sorted[sorted.length - 1], + } + } + + getAllStats() { + const stats: Record = {} + for (const [name] of this.metrics) { + stats[name] = this.getStats(name) + } + return stats + } +} + +export const metrics = new MetricsCollector() + +// Metrics endpoint +// routes/metrics.ts +export const Route = createFileRoute('/metrics')({ + server: { + handlers: { + GET: async () => { + return json({ + system: { + uptime: process.uptime(), + memory: process.memoryUsage(), + timestamp: new Date().toISOString(), + }, + application: metrics.getAllStats(), + }) + }, + }, + }, +}) +``` + +### Debug Headers for Development + +Add helpful debug information to responses: + +```tsx +import { createMiddleware } from '@tanstack/solid-start' + +const debugMiddleware = createMiddleware().handler(async ({ next }) => { + const response = await next() + + if (process.env.NODE_ENV === 'development') { + response.headers.set('X-Debug-Timestamp', new Date().toISOString()) + response.headers.set('X-Debug-Node-Version', process.version) + response.headers.set('X-Debug-Uptime', process.uptime().toString()) + } + + return response +}) +``` + +### Environment-Specific Logging + +Configure different logging strategies for development vs production: + +```tsx +// utils/logger.ts +import { createIsomorphicFn } from '@tanstack/solid-start' + +type LogLevel = 'debug' | 'info' | 'warn' | 'error' + +const logger = createIsomorphicFn() + .server((level: LogLevel, message: string, data?: any) => { + const timestamp = new Date().toISOString() + + if (process.env.NODE_ENV === 'development') { + // Development: Detailed console logging + console[level](`[${timestamp}] [${level.toUpperCase()}]`, message, data) + } else { + // Production: Structured JSON logging + console.log( + JSON.stringify({ + timestamp, + level, + message, + data, + service: 'tanstack-start', + environment: process.env.NODE_ENV, + }), + ) + } + }) + .client((level: LogLevel, message: string, data?: any) => { + if (process.env.NODE_ENV === 'development') { + console[level](`[CLIENT] [${level.toUpperCase()}]`, message, data) + } else { + // Production: Send to analytics service + // analytics.track('client_log', { level, message, data }) + } + }) + +// Usage anywhere in your app +export { logger } + +// Example usage +const fetchUserData = createServerFn().handler(async ({ data: userId }) => { + logger('info', 'Fetching user data', { userId }) + + try { + const user = await db.users.findUnique({ where: { id: userId } }) + logger('info', 'User data fetched successfully', { userId }) + return user + } catch (error) { + logger('error', 'Failed to fetch user data', { + userId, + error: error.message, + }) + throw error + } +}) +``` + +### Simple Error Reporting + +Basic error reporting without external dependencies: + +```tsx +// utils/error-reporter.ts +const errorStore = new Map< + string, + { count: number; lastSeen: Date; error: any } +>() + +export function reportError(error: Error, context?: any) { + const key = `${error.name}:${error.message}` + const existing = errorStore.get(key) + + if (existing) { + existing.count++ + existing.lastSeen = new Date() + } else { + errorStore.set(key, { + count: 1, + lastSeen: new Date(), + error: { + name: error.name, + message: error.message, + stack: error.stack, + context, + }, + }) + } + + // Log immediately + console.error('[ERROR REPORTED]:', { + error: error.message, + count: existing ? existing.count : 1, + context, + }) +} + +// Error reporting endpoint +// routes/errors.ts +export const Route = createFileRoute('/admin/errors')({ + server: { + handlers: { + GET: async () => { + const errors = Array.from(errorStore.entries()).map(([key, data]) => ({ + id: key, + ...data, + })) + + return json({ errors }) + }, + }, + }, +}) +``` + +## External Observability Tools + +While TanStack Start provides built-in observability patterns, external tools offer more comprehensive monitoring: + +### Other Popular Tools + +**Application Performance Monitoring:** + +- **[DataDog](https://www.datadoghq.com/)** - Full-stack monitoring with APM +- **[New Relic](https://newrelic.com/)** - Performance monitoring and alerting +- **[Honeycomb](https://honeycomb.io/)** - Observability for complex systems + +**Error Tracking:** + +- **[Bugsnag](https://bugsnag.com/)** - Error monitoring with deployment tracking +- **[Rollbar](https://rollbar.com/)** - Real-time error alerting + +**Analytics & User Behavior:** + +- **[PostHog](https://posthog.com/)** - Product analytics with error tracking +- **[Mixpanel](https://mixpanel.com/)** - Event tracking and user analytics + +### OpenTelemetry Integration (Experimental) + +[OpenTelemetry](https://opentelemetry.io/) is the industry standard for observability. Here's an experimental approach to integrate it with TanStack Start: + +```tsx +// instrumentation.ts - Initialize before your app +import { NodeSDK } from '@opentelemetry/sdk-node' +import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node' +import { Resource } from '@opentelemetry/resources' +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions' + +const sdk = new NodeSDK({ + resource: new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: 'tanstack-start-app', + [SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0', + }), + instrumentations: [getNodeAutoInstrumentations()], +}) + +// Initialize BEFORE importing your app +sdk.start() +``` + +```tsx +// Server function tracing +import { trace, SpanStatusCode } from '@opentelemetry/api' + +const tracer = trace.getTracer('tanstack-start') + +const getUserWithTracing = createServerFn({ method: 'GET' }) + .inputValidator((id: string) => id) + .handler(async ({ data: id }) => { + return tracer.startActiveSpan('get-user', async (span) => { + span.setAttributes({ + 'user.id': id, + operation: 'database.query', + }) + + try { + const user = await db.users.findUnique({ where: { id } }) + span.setStatus({ code: SpanStatusCode.OK }) + return user + } catch (error) { + span.recordException(error) + span.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message, + }) + throw error + } finally { + span.end() + } + }) + }) +``` + +```tsx +// Middleware for automatic tracing +import { createMiddleware } from '@tanstack/solid-start' +import { trace, SpanStatusCode } from '@opentelemetry/api' + +const tracer = trace.getTracer('tanstack-start') + +const tracingMiddleware = createMiddleware().handler( + async ({ next, request }) => { + const url = new URL(request.url) + + return tracer.startActiveSpan( + `${request.method} ${url.pathname}`, + async (span) => { + span.setAttributes({ + 'http.method': request.method, + 'http.url': request.url, + 'http.route': url.pathname, + }) + + try { + const response = await next() + span.setAttribute('http.status_code', response.status) + span.setStatus({ code: SpanStatusCode.OK }) + return response + } catch (error) { + span.recordException(error) + span.setStatus({ + code: SpanStatusCode.ERROR, + message: error.message, + }) + throw error + } finally { + span.end() + } + }, + ) + }, +) +``` + +> **Note**: The above OpenTelemetry integration is experimental and requires manual setup. We're exploring first-class OpenTelemetry support that would provide automatic instrumentation for server functions, middleware, and route loaders. + +### Quick Integration Pattern + +Most observability tools follow a similar integration pattern with TanStack Start: + +```tsx +// Initialize in app entry point +import { initObservabilityTool } from 'your-tool' + +initObservabilityTool({ + dsn: import.meta.env.VITE_TOOL_DSN, + environment: import.meta.env.NODE_ENV, +}) + +// Server function middleware +const observabilityMiddleware = createMiddleware().handler(async ({ next }) => { + return yourTool.withTracing('server-function', async () => { + try { + return await next() + } catch (error) { + yourTool.captureException(error) + throw error + } + }) +}) +``` + +## Best Practices + +### Development vs Production + +```tsx +// Different strategies per environment +const observabilityConfig = { + development: { + logLevel: 'debug', + enableTracing: true, + enableMetrics: false, // Too noisy in dev + }, + production: { + logLevel: 'warn', + enableTracing: true, + enableMetrics: true, + enableAlerting: true, + }, +} +``` + +### Performance Monitoring Checklist + +- [ ] **Server Function Performance**: Track execution times +- [ ] **Route Loading Times**: Monitor loader performance +- [ ] **Database Query Performance**: Log slow queries +- [ ] **External API Latency**: Monitor third-party service calls +- [ ] **Memory Usage**: Track memory consumption patterns +- [ ] **Error Rates**: Monitor error frequency and types + +### Security Considerations + +- Never log sensitive data (passwords, tokens, PII) +- Use structured logging for better parsing +- Implement log rotation in production +- Consider compliance requirements (GDPR, CCPA) + +## Future OpenTelemetry Support + +Direct OpenTelemetry support is coming to TanStack Start, which will provide automatic instrumentation for server functions, middleware, and route loaders without the manual setup shown above. + +## Resources + +- **[Sentry Documentation](https://docs.sentry.io/)** +- **[OpenTelemetry Documentation](https://opentelemetry.io/docs/)** - Industry standard observability +- **[Working Example](https://github.com/TanStack/router/tree/main/examples/solid/start-basic)** - See observability patterns in action diff --git a/docs/start/framework/solid/quick-start.md b/docs/start/framework/solid/quick-start.md index 3789efe9629..2948e668f7d 100644 --- a/docs/start/framework/solid/quick-start.md +++ b/docs/start/framework/solid/quick-start.md @@ -1,9 +1,63 @@ --- -ref: docs/start/framework/react/quick-start.md -replace: - { - '@tanstack/react-start': '@tanstack/solid-start', - 'React': 'SolidJS', - 'react-router': 'solid-router', - } +id: quick-start +title: Quick Start --- + +## Impatient? + +The fastest way to get a Start project up and running is with the cli. Just run + +``` +pnpm create @tanstack/start@latest +``` + +or + +``` +npm create @tanstack/start@latest +``` + +depending on your package manage of choice. You'll be prompted to add things like Tailwind, eslint, and a ton of other options. + +You can also clone and run the [Basic](https://github.com/TanStack/router/tree/main/examples/solid/start-basic) example right away with the following commands: + +```bash +npx gitpick TanStack/router/tree/main/examples/solid/start-basic start-basic +cd start-basic +npm install +npm run dev +``` + +If you'd like to use a different example, you can replace `start-basic` above with the slug of the example you'd like to use from the list below. + +Once you've cloned the example you want, head back to the [Routing](../routing) guide to learn how to use TanStack Start! + +## Examples + +TanStack Start has load of examples to get you started. Pick one of the examples below to get started! + +- [Bare](https://github.com/TanStack/router/tree/main/examples/solid/start-bare) (start-bare) +- [Basic](https://github.com/TanStack/router/tree/main/examples/solid/start-basic) (start-basic) +- [Basic Static](https://github.com/TanStack/router/tree/main/examples/solid/start-basic-stats) (start-basic-static) +- [Counter](https://github.com/TanStack/router/tree/main/examples/solid/start-counter) (start-counter) + +### Stackblitz + +Each example above has an embedded stackblitz preview to find the one that feels like a good starting point + +### Quick Deploy + +To quickly deploy an example, click the **Deploy to Netlify** button on an example's page to both clone and deploy the example to Netlify. + +### Manual Deploy + +To manually clone and deploy the example to anywhere else you'd like, use the following commands replacing `EXAMPLE_SLUG` with the slug of the example you'd like to use from above: + +```bash +npx gitpick TanStack/router/tree/main/examples/solid/EXAMPLE_SLUG my-new-project +cd my-new-project +npm install +npm run dev +``` + +Once you've clone or deployed an example, head back to the [Routing](../routing) guide to learn how to use TanStack Start! diff --git a/docs/start/framework/solid/routing.md b/docs/start/framework/solid/routing.md index 51cde2dddcd..1828f298e31 100644 --- a/docs/start/framework/solid/routing.md +++ b/docs/start/framework/solid/routing.md @@ -1,9 +1,231 @@ --- -ref: docs/start/framework/react/routing.md -replace: - { - '@tanstack/react-start': '@tanstack/solid-start', - 'React': 'SolidJS', - 'react-router': 'solid-router', - } +id: routing +title: Routing --- + +TanStack Start is built on top of TanStack Router, so all of the features of TanStack Router are available to you. + +> [!NOTE] +> We highly recommend reading the [TanStack Router documentation](/router/latest/docs/framework/solid/overview) to learn more about the features and capabilities of TanStack Router. What you learn here is more of a high-level overview of TanStack Router and how it works in Start. + +## The Router + +The `router.tsx` file is the file that will dictate the behavior of TanStack Router used within Start. It's located in the `src` directory of your project. + +``` +src/ +├── router.tsx +``` + +Here, you can configure everything from the default [preloading functionality](/router/latest/docs/framework/solid/guide/preloading) to [caching staleness](/router/latest/docs/framework/solid/guide/data-loading). + +```tsx +// src/router.tsx +import { createRouter } from '@tanstack/solid-router' +import { routeTree } from './routeTree.gen' + +// You must export a getRouter function that +// returns a new router instance each time +export function getRouter() { + const router = createRouter({ + routeTree, + scrollRestoration: true, + }) + + return router +} +``` + +## File-Based Routing + +Start uses TanStack Router's file-based routing approach to ensure proper code-splitting and advanced type-safety. + +You can find your routes in the `src/routes` directory. + +``` +src/ +├── routes <-- This is where you put your routes +│ ├── __root.tsx +│ ├── index.tsx +│ ├── about.tsx +│ ├── posts.tsx +│ ├── posts/$postId.tsx +``` + +## The Root Route + +The root route is the top-most route in the entire tree and encapsulates all other routes as children. It's found in the `src/routes/__root.tsx` file and must be named `__root.tsx`. + +``` +src/ +├── routes +│ ├── __root.tsx <-- The root route +``` + +- It has no path and is **always** matched +- Its `component` is **always** rendered +- This is where you render your document shell, e.g. ``, ``, etc. +- Because it is **always rendered**, it is the perfect place to construct your application shell and take care of any global logic + +```tsx +// src/routes/__root.tsx +import { + Outlet, + createRootRoute, + HeadContent, + Scripts, +} from '@tanstack/solid-router' +import type { solidNode } from 'solid' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { + charSet: 'utf-8', + }, + { + name: 'viewport', + content: 'width=device-width, initial-scale=1', + }, + { + title: 'TanStack Start Starter', + }, + ], + }), + component: RootComponent, +}) + +function RootComponent() { + return ( + + + + ) +} + +function RootDocument({ children }: Readonly<{ children: Solid.JSX.Element }>) { + return ( + <> + + {children} + + + ) +} +``` + +Notice the `Scripts` component at the bottom. This is used to load all of the client-side JavaScript for the application and should always be included for proper functionality. + +## The HeadContent Component + +The `HeadContent` component is used to render the head, title, meta, link, and head-related script tags of the document. + +It should be **rendered in the `` tag of your root route's layout.** + +## The Outlet Component + +The `Outlet` component is used to render the next potentially matching child route. `` doesn't take any props and can be rendered anywhere within a route's component tree. If there is no matching child route, `` will render `null`. + +## The Scripts Component + +The `Scripts` component is used to render the body scripts of the document. + +It should be **rendered in the `` tag of your root route's layout.** + +## Route Tree Generation + +You may notice a `routeTree.gen.ts` file in your project. + +``` +src/ +├── routeTree.gen.ts <-- The generated route tree file +``` + +This file is automatically generated when you run TanStack Start (via `npm run dev` or `npm run start`). This file contains the generated route tree and a handful of TS utilities that make TanStack Start's type-safety extremely fast and fully inferred. + +**You may gitignore this file, since it is a build artifact.** + +## Nested Routing + +TanStack Router uses nested routing to match the URL with the correct component tree to render. + +For example, given the following routes: + +``` +routes/ +├── __root.tsx <-- Renders the component +├── posts.tsx <-- Renders the component +├── posts.$postId.tsx <-- Renders the component +``` + +And the URL: `/posts/123` + +The component tree would look like this: + +``` + + + + + +``` + +## Types of Routes + +There are a few different types of routes that you can create in your project. + +- Index Routes - Matched when the URL is exactly the same as the route's path +- Dynamic/Wildcard/Splat Routes - Dynamically capture part or all of the URL path into a variable to use in your application + +There are also a few different utility route types that you can use to group and organize your routes + +- Pathless Layout Routes (Apply layout or logic to a group of routes without nesting them in a path) +- Non-Nested Routes (Un-nest a route from its parents and render its own component tree) +- Grouped Routes (Group routes together in a directory simply for organization, without affecting the path hierarchy) + +## Route Tree Configuration + +The route tree is configured in the `src/routes` directory. + +## Creating File Routes + +To create a route, create a new file that corresponds to the path of the route you want to create. For example: + +| Path | Filename | Type | +| ---------------- | ------------------- | -------------- | +| `/` | `index.tsx` | Index Route | +| `/about` | `about.tsx` | Static Route | +| | `posts.tsx` | "Layout" Route | +| `/posts/` | `posts/index.tsx` | Index Route | +| `/posts/:postId` | `posts/$postId.tsx` | Dynamic Route | +| `/rest/*` | `rest/$.tsx` | Wildcard Route | + +## Defining Routes + +To define a route, use the `createFileRoute` function to export the route as the `Route` variable. + +For example, to handle the `/posts/:postId` route, you would create a file named `posts/$postId.tsx` here: + +``` +src/ +├── routes +│ ├── posts/$postId.tsx +``` + +Then, define the route like this: + +```tsx +// src/routes/posts/$postId.tsx +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/posts/$postId')({ + component: PostComponent, +}) +``` + +> [!NOTE] +> The path string passed to `createFileRoute` is **automatically written and managed by the router for you via the TanStack Router Bundler Plugin or Router CLI.** So, as you create new routes, move routes around or rename routes, the path will be updated for you automatically. + +## This is just the "start" + +This has been just a high-level overview of how to configure routes using TanStack Router. For more detailed information, please refer to the [TanStack Router documentation](/router/latest/docs/framework/solid/routing/file-based-routing). diff --git a/docs/start/framework/solid/selective-ssr.md b/docs/start/framework/solid/selective-ssr.md index 2586080db1c..609614ed153 100644 --- a/docs/start/framework/solid/selective-ssr.md +++ b/docs/start/framework/solid/selective-ssr.md @@ -1,5 +1,222 @@ --- -ref: docs/start/framework/react/selective-ssr.md -replace: - { '@tanstack/react-start': '@tanstack/solid-start', 'React': 'SolidJS' } +id: selective-ssr +title: Selective Server-Side Rendering (SSR) --- + +## What is Selective SSR? + +In TanStack Start, routes matching the initial request are rendered on the server by default. This means `beforeLoad` and `loader` are executed on the server, followed by rendering the route components. The resulting HTML is sent to the client, which hydrates the markup into a fully interactive application. + +However, there are cases where you might want to disable SSR for certain routes or all routes, such as: + +- When `beforeLoad` or `loader` requires browser-only APIs (e.g., `localStorage`). +- When the route component depends on browser-only APIs (e.g., `canvas`). + +TanStack Start's Selective SSR feature lets you configure: + +- Which routes should execute `beforeLoad` or `loader` on the server. +- Which route components should be rendered on the server. + +## How does this compare to SPA mode? + +TanStack Start's [SPA mode](../spa-mode) completely disables server-side execution of `beforeLoad` and `loader`, as well as server-side rendering of route components. Selective SSR allows you to configure server-side handling on a per-route basis, either statically or dynamically. + +## Configuration + +You can control how a route is handled during the initial server request using the `ssr` property. If this property is not set, it defaults to `true`. You can change this default using the `defaultSsr` option in `createStart`: + +```tsx +// src/start.ts +import { createStart } from '@tanstack/solid-start' + +export const startInstance = createStart(() => ({ + // Disable SSR by default + defaultSsr: false, +})) +``` + +### `ssr: true` + +This is the default behavior unless otherwise configured. On the initial request, it will: + +- Run `beforeLoad` on the server and send the resulting context to the client. +- Run `loader` on the server and send the loader data to the client. +- Render the component on the server and send the HTML markup to the client. + +```tsx +// src/routes/posts/$postId.tsx +export const Route = createFileRoute('/posts/$postId')({ + ssr: true, + beforeLoad: () => { + console.log('Executes on the server during the initial request') + console.log('Executes on the client for subsequent navigation') + }, + loader: () => { + console.log('Executes on the server during the initial request') + console.log('Executes on the client for subsequent navigation') + }, + component: () =>
This component is rendered on the server
, +}) +``` + +### `ssr: false` + +This disables server-side: + +- Execution of the route's `beforeLoad` and `loader`. +- Rendering of the route component. + +```tsx +// src/routes/posts/$postId.tsx +export const Route = createFileRoute('/posts/$postId')({ + ssr: false, + beforeLoad: () => { + console.log('Executes on the client during hydration') + }, + loader: () => { + console.log('Executes on the client during hydration') + }, + component: () =>
This component is rendered on the client
, +}) +``` + +### `ssr: 'data-only'` + +This hybrid option will: + +- Run `beforeLoad` on the server and send the resulting context to the client. +- Run `loader` on the server and send the loader data to the client. +- Disable server-side rendering of the route component. + +```tsx +// src/routes/posts/$postId.tsx +export const Route = createFileRoute('/posts/$postId')({ + ssr: 'data-only', + beforeLoad: () => { + console.log('Executes on the server during the initial request') + console.log('Executes on the client for subsequent navigation') + }, + loader: () => { + console.log('Executes on the server during the initial request') + console.log('Executes on the client for subsequent navigation') + }, + component: () =>
This component is rendered on the client
, +}) +``` + +### Functional Form + +For more flexibility, you can use the functional form of the `ssr` property to decide at runtime whether to SSR a route: + +```tsx +// src/routes/docs/$docType/$docId.tsx +export const Route = createFileRoute('/docs/$docType/$docId')({ + validateSearch: z.object({ details: z.boolean().optional() }), + ssr: ({ params, search }) => { + if (params.status === 'success' && params.value.docType === 'sheet') { + return false + } + if (search.status === 'success' && search.value.details) { + return 'data-only' + } + }, + beforeLoad: () => { + console.log('Executes on the server depending on the result of ssr()') + }, + loader: () => { + console.log('Executes on the server depending on the result of ssr()') + }, + component: () =>
This component is rendered on the client
, +}) +``` + +The `ssr` function runs only on the server during the initial request and is stripped from the client bundle. + +`search` and `params` are passed in after validation as a discriminated union: + +```tsx +params: + | { status: 'success'; value: Expand> } + | { status: 'error'; error: unknown } +search: + | { status: 'success'; value: Expand> } + | { status: 'error'; error: unknown } +``` + +If validation fails, `status` will be `error` and `error` will contain the failure details. Otherwise, `status` will be `success` and `value` will contain the validated data. + +### Inheritance + +At runtime, a child route inherits the Selective SSR configuration of its parent. However, the inherited value can only be changed to be more restrictive (i.e. `true` to `data-only` or `false` and `data-only` to `false`). For example: + +```tsx +root { ssr: undefined } + posts { ssr: false } + $postId { ssr: true } +``` + +- `root` defaults to `ssr: true`. +- `posts` explicitly sets `ssr: false`, so neither `beforeLoad` nor `loader` will run on the server, and the route component won't be rendered on the server. +- `$postId` sets `ssr: true`, but inherits `ssr: false` from its parent. Because the inherited value can only be changed to be more restrictive, `ssr: true` has no effect and the inherited `ssr: false` will remain. + +Another example: + +```tsx +root { ssr: undefined } + posts { ssr: 'data-only' } + $postId { ssr: true } + details { ssr: false } +``` + +- `root` defaults to `ssr: true`. +- `posts` sets `ssr: 'data-only'`, so `beforeLoad` and `loader` run on the server, but the route component isn't rendered on the server. +- `$postId` sets `ssr: true`, but inherits `ssr: 'data-only'` from its parent. +- `details` sets `ssr: false`, so neither `beforeLoad` nor `loader` will run on the server, and the route component won't be rendered on the server. Here the inherited value is changed to be more restrictive, and therefore, the `ssr: false` will override the inherited value. + +## Fallback Rendering + +For the first route with `ssr: false` or `ssr: 'data-only'`, the server will render the route's `pendingComponent` as a fallback. If `pendingComponent` isn't configured, the `defaultPendingComponent` will be rendered. If neither is configured, no fallback will be rendered. + +On the client during hydration, this fallback will be displayed for at least `minPendingMs` (or `defaultPendingMinMs` if not configured), even if the route doesn't have `beforeLoad` or `loader` defined. + +## How to disable SSR of the root route? + +You can disable server side rendering of the root route component, however the `` shell still needs to be rendered on the server. This shell is configured via the `shellComponent` property and takes a single property `children`. The `shellComponent` is always SSRed and is wrapping around the root `component`, the root `errorComponent` or the root `notFound` component respectively. + +A minimal setup of a root route with disabled SSR for the route component looks like this: + +```tsx +import * as Solid from 'solid-js' + +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from '@tanstack/solid-router' + +export const Route = createRootRoute({ + shellComponent: RootShell, + component: RootComponent, + errorComponent: () =>
Error
, + notFoundComponent: () =>
Not found
, + ssr: false, // or `defaultSsr: false` on the router +}) + +function RootShell(props) { + return ( + + {props.children} + + ) +} + +function RootComponent() { + return ( +
+

This component will be rendered on the client

+ +
+ ) +} +``` diff --git a/docs/start/framework/solid/server-functions.md b/docs/start/framework/solid/server-functions.md index 177d665a31c..46572c0d0a6 100644 --- a/docs/start/framework/solid/server-functions.md +++ b/docs/start/framework/solid/server-functions.md @@ -1,5 +1,9 @@ --- ref: docs/start/framework/react/server-functions.md replace: - { '@tanstack/react-start': '@tanstack/solid-start', 'React': 'SolidJS' } + { + '@tanstack/react-start': '@tanstack/solid-start', + 'React': 'SolidJS', + '@tanstack/react-router': '@tanstack/solid-router', + } --- diff --git a/docs/start/framework/solid/server-routes.md b/docs/start/framework/solid/server-routes.md index d089b385dc5..eabd423f29a 100644 --- a/docs/start/framework/solid/server-routes.md +++ b/docs/start/framework/solid/server-routes.md @@ -1,5 +1,491 @@ --- -ref: docs/start/framework/react/server-routes.md -replace: - { '@tanstack/react-start': '@tanstack/solid-start', 'React': 'SolidJS' } +id: server-routes +title: Server Routes --- + +Server routes are a powerful feature of TanStack Start that allow you to create server-side endpoints in your application and are useful for handling raw HTTP requests, form submissions, user authentication, and much more. + +Server routes can be defined in your `./src/routes` directory of your project **right alongside your TanStack Router routes** and are automatically handled by the TanStack Start server. + +Here's what a simple server route looks like: + +```ts +// routes/hello.ts +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/hello')({ + server: { + handlers: { + GET: async ({ request }) => { + return new Response('Hello, World!') + }, + }, + }, +}) +``` + +## Server Routes and App Routes + +Because server routes can be defined in the same directory as your app routes, you can even use the same file for both! + +```tsx +// routes/hello.tsx +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/hello')({ + server: { + handlers: { + POST: async ({ request }) => { + const body = await request.json() + return new Response(JSON.stringify({ message: `Hello, ${body.name}!` })) + }, + }, + }, + component: HelloComponent, +}) + +function HelloComponent() { + const [reply, setReply] = createSignal('') + + return ( +
+ +
+ ) +} +``` + +## File Route Conventions + +Server routes in TanStack Start follow the same file-based routing conventions as TanStack Router. This means that each file in your `routes` directory with a `server` property in the `createFileRoute` call will be treated as an API route. Here are a few examples: + +- `/routes/users.ts` will create an API route at `/users` +- `/routes/users.index.ts` will **also** create an API route at `/users` (but will error if duplicate methods are defined) +- `/routes/users/$id.ts` will create an API route at `/users/$id` +- `/routes/users/$id/posts.ts` will create an API route at `/users/$id/posts` +- `/routes/users.$id.posts.ts` will create an API route at `/users/$id/posts` +- `/routes/api/file/$.ts` will create an API route at `/api/file/$` +- `/routes/my-script[.]js.ts` will create an API route at `/my-script.js` + +## Unique Route Paths + +Each route can only have a single handler file associated with it. So, if you have a file named `routes/users.ts` which'd equal the request path of `/users`, you cannot have other files that'd also resolve to the same route. For example, the following files would all resolve to the same route and would error: + +- `/routes/users.index.ts` +- `/routes/users.ts` +- `/routes/users/index.ts` + +## Escaped Matching + +Just as with normal routes, server routes can match on escaped characters. For example, a file named `routes/users[.]json.ts` will create an API route at `/users.json`. + +## Pathless Layout Routes and Break-out Routes + +Because of the unified routing system, pathless layout routes and break-out routes are supported for similar functionality around server route middleware. + +- Pathless layout routes can be used to add middleware to a group of routes +- Break-out routes can be used to "break out" of parent middleware + +## Nested Directories vs File-names + +In the examples above, you may have noticed that the file naming conventions are flexible and allow you to mix and match directories and file names. This is intentional and allows you to organize your Server routes in a way that makes sense for your application. You can read more about this in the [TanStack Router File-based Routing Guide](/router/latest/docs/framework/solid/routing/file-based-routing#s-or-s). + +## Handling Server Route Requests + +Server route requests are handled by Start automatically by default or by Start's `createStartHandler` in your custom `src/server.ts` entry point file. + +The start handler is responsible for matching an incoming request to a server route and executing the appropriate middleware and handler. + +If you need to customize the server handler, you can do so by creating a custom handler and then passing the event to the start handler. See [The Server Entry Point](../server-entry-point). + +## Defining a Server Route + +Server routes are created by adding a `server` property to your `createFileRoute` call. The `server` property contains: + +- `handlers` - Either an object mapping HTTP methods to handler functions, or a function that receives `createHandlers` for more advanced use cases +- `middleware` - Optional route-level middleware array that applies to all handlers + +```ts +// routes/hello.ts +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/hello')({ + server: { + handlers: { + GET: async ({ request }) => { + return new Response('Hello, World! from ' + request.url) + }, + }, + }, +}) +``` + +## Defining Server Route Handlers + +You can define handlers in two ways: + +- **Simple handlers**: Provide handler functions directly in a handlers object +- **Handlers with middleware**: Use the `createHandlers` function to define handlers with middleware + +### Simple handlers + +For simple use cases, you can provide handler functions directly in a handlers object. + +```ts +// routes/hello.ts +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/hello')({ + server: { + handlers: { + GET: async ({ request }) => { + return new Response('Hello, World! from ' + request.url) + }, + }, + }, +}) +``` + +### Adding middleware to specific handlers + +For more complex use cases, you can add middleware to specific handlers. This requires using the `createHandlers` function: + +```tsx +// routes/hello.ts +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/hello')({ + server: { + handlers: ({ createHandlers }) => + createHandlers({ + GET: { + middleware: [loggerMiddleware], + handler: async ({ request }) => { + return new Response('Hello, World! from ' + request.url) + }, + }, + }), + }, +}) +``` + +### Adding middleware to all handlers + +You can also add middleware that applies to all handlers in a route by using the `middleware` property at the server level: + +```tsx +// routes/hello.ts +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/hello')({ + server: { + middleware: [authMiddleware, loggerMiddleware], // Applies to all handlers + handlers: { + GET: async ({ request }) => { + return new Response('Hello, World! from ' + request.url) + }, + POST: async ({ request }) => { + const body = await request.json() + return new Response(`Hello, ${body.name}!`) + }, + }, + }, +}) +``` + +### Combining route-level and handler-specific middleware + +You can combine both approaches - route-level middleware will run first, followed by handler-specific middleware: + +```tsx +// routes/hello.ts +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/hello')({ + server: { + middleware: [authMiddleware], // Runs first for all handlers + handlers: ({ createHandlers }) => + createHandlers({ + GET: async ({ request }) => { + return new Response('Hello, World!') + }, + POST: { + middleware: [validationMiddleware], // Runs after authMiddleware, only for POST + handler: async ({ request }) => { + const body = await request.json() + return new Response(`Hello, ${body.name}!`) + }, + }, + }), + }, +}) +``` + +## Handler Context + +Each HTTP method handler receives an object with the following properties: + +- `request`: The incoming request object. You can read more about the `Request` object in the [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/Request). +- `params`: An object containing the dynamic path parameters of the route. For example, if the route path is `/users/$id`, and the request is made to `/users/123`, then `params` will be `{ id: '123' }`. We'll cover dynamic path parameters and wildcard parameters later in this guide. +- `context`: An object containing the context of the request. This is useful for passing data between middleware. + +Once you've processed the request, you can return a `Response` object or `Promise` or even use any of the helpers from `@tanstack/solid-start` to manipulate the response. + +## Dynamic Path Params + +Server routes support dynamic path parameters in the same way as TanStack Router. For example, a file named `routes/users/$id.ts` will create an API route at `/users/$id` that accepts a dynamic `id` parameter. + +```ts +// routes/users/$id.ts +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/users/$id')({ + server: { + handlers: { + GET: async ({ params }) => { + const { id } = params + return new Response(`User ID: ${id}`) + }, + }, + }, +}) + +// Visit /users/123 to see the response +// User ID: 123 +``` + +You can also have multiple dynamic path parameters in a single route. For example, a file named `routes/users/$id/posts/$postId.ts` will create an API route at `/users/$id/posts/$postId` that accepts two dynamic parameters. + +```ts +// routes/users/$id/posts/$postId.ts +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/users/$id/posts/$postId')({ + server: { + handlers: { + GET: async ({ params }) => { + const { id, postId } = params + return new Response(`User ID: ${id}, Post ID: ${postId}`) + }, + }, + }, +}) + +// Visit /users/123/posts/456 to see the response +// User ID: 123, Post ID: 456 +``` + +## Wildcard/Splat Param + +Server routes also support wildcard parameters at the end of the path, which are denoted by a `$` followed by nothing. For example, a file named `routes/file/$.ts` will create an API route at `/file/$` that accepts a wildcard parameter. + +```ts +// routes/file/$.ts +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/file/$')({ + server: { + handlers: { + GET: async ({ params }) => { + const { _splat } = params + return new Response(`File: ${_splat}`) + }, + }, + }, +}) + +// Visit /file/hello.txt to see the response +// File: hello.txt +``` + +## Handling requests with a body + +To handle POST requests,you can add a `POST` handler to the route object. The handler will receive the request object as the first argument, and you can access the request body using the `request.json()` method. + +```ts +// routes/hello.ts +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/hello')({ + server: { + handlers: { + POST: async ({ request }) => { + const body = await request.json() + return new Response(`Hello, ${body.name}!`) + }, + }, + }, +}) + +// Send a POST request to /hello with a JSON body like { "name": "Tanner" } +// Hello, Tanner! +``` + +This also applies to other HTTP methods like `PUT`, `PATCH`, and `DELETE`. You can add handlers for these methods in the route object and access the request body using the appropriate method. + +It's important to remember that the `request.json()` method returns a `Promise` that resolves to the parsed JSON body of the request. You need to `await` the result to access the body. + +This is a common pattern for handling POST requests in Server routes/ You can also use other methods like `request.text()` or `request.formData()` to access the body of the request. + +## Responding with JSON + +When returning JSON using a Response object, this is a common pattern: + +```ts +// routes/hello.ts +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/hello')({ + server: { + handlers: { + GET: async ({ request }) => { + return new Response(JSON.stringify({ message: 'Hello, World!' }), { + headers: { + 'Content-Type': 'application/json', + }, + }) + }, + }, + }, +}) + +// Visit /hello to see the response +// {"message":"Hello, World!"} +``` + +## Using the `json` helper function + +Or you can use the `json` helper function to automatically set the `Content-Type` header to `application/json` and serialize the JSON object for you. + +```ts +// routes/hello.ts +import { createFileRoute } from '@tanstack/solid-router' +import { json } from '@tanstack/solid-start' + +export const Route = createFileRoute('/hello')({ + server: { + handlers: { + GET: async ({ request }) => { + return json({ message: 'Hello, World!' }) + }, + }, + }, +}) + +// Visit /hello to see the response +// {"message":"Hello, World!"} +``` + +## Responding with a status code + +You can set the status code of the response by either: + +- Passing it as a property of the second argument to the `Response` constructor + + ```ts + // routes/hello.ts + import { createFileRoute } from '@tanstack/solid-router' + import { json } from '@tanstack/solid-start' + + export const Route = createFileRoute('/hello')({ + server: { + handlers: { + GET: async ({ request, params }) => { + const user = await findUser(params.id) + if (!user) { + return new Response('User not found', { + status: 404, + }) + } + return json(user) + }, + }, + }, + }) + ``` + +- Using the `setResponseStatus` helper function from `@tanstack/solid-start/server` + + ```ts + // routes/hello.ts + import { createFileRoute } from '@tanstack/solid-router' + import { json } from '@tanstack/solid-start' + import { setResponseStatus } from '@tanstack/solid-start/server' + + export const Route = createFileRoute('/hello')({ + server: { + handlers: { + GET: async ({ request, params }) => { + const user = await findUser(params.id) + if (!user) { + setResponseStatus(404) + return new Response('User not found') + } + return json(user) + }, + }, + }, + }) + ``` + +In this example, we're returning a `404` status code if the user is not found. You can set any valid HTTP status code using this method. + +## Setting headers in the response + +Sometimes you may need to set headers in the response. You can do this by either: + +- Passing an object as the second argument to the `Response` constructor. + + ```ts + // routes/hello.ts + import { createFileRoute } from '@tanstack/solid-router' + export const Route = createFileRoute('/hello')({ + server: { + handlers: { + GET: async ({ request }) => { + return new Response('Hello, World!', { + headers: { + 'Content-Type': 'text/plain', + }, + }) + }, + }, + }) + // Visit /hello to see the response + // Hello, World! + ``` + +- Or using the `setResponseHeaders` helper function from `@tanstack/solid-start/server`. + +```ts +// routes/hello.ts +import { createFileRoute } from '@tanstack/solid-router' +import { setResponseHeaders } from '@tanstack/solid-start/server' + +export const Route = createFileRoute('/hello')({ + server: { + handlers: { + GET: async ({ request }) => { + setResponseHeaders({ + 'Content-Type': 'text/plain', + }) + return new Response('Hello, World!') + }, + }, + }, +}) +``` diff --git a/docs/start/framework/solid/static-prerendering.md b/docs/start/framework/solid/static-prerendering.md index 65a2fafb0d4..7c79b6995d9 100644 --- a/docs/start/framework/solid/static-prerendering.md +++ b/docs/start/framework/solid/static-prerendering.md @@ -1,5 +1,60 @@ --- -ref: docs/start/framework/react/static-prerendering.md -replace: - { '@tanstack/react-start': '@tanstack/solid-start', 'React': 'SolidJS' } +id: static-prerendering +title: Static Prerendering --- + +Static prerendering is the process of generating static HTML files for your application. This can be useful for either improving the performance of your application, as it allows you to serve pre-rendered HTML files to users without having to generate them on the fly or for deploying static sites to platforms that do not support server-side rendering. + +## Prerendering + +TanStack Start can prerender your application to static HTML files, which can then be served to users without having to generate them on the fly. To prerender your application, you can add the `prerender` option to your tanstackStart configuration in `vite.config.ts` file: + +```ts +// vite.config.ts + +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import viteSolid from 'vite-plugin-solid' + +export default defineConfig({ + plugins: [ + tanstackStart({ + prerender: { + // Enable prerendering + enabled: true, + + // Enable if you need pages to be at `/page/index.html` instead of `/page.html` + autoSubfolderIndex: true, + + // How many prerender jobs to run at once + concurrency: 14, + + // Whether to extract links from the HTML and prerender them also + crawlLinks: true, + + // Filter function takes the page object and returns whether it should prerender + filter: ({ path }) => !path.startsWith('/do-not-render-me'), + + // Number of times to retry a failed prerender job + retryCount: 2, + + // Delay between retries in milliseconds + retryDelay: 1000, + + // Callback when page is successfully rendered + onSuccess: ({ page }) => { + console.log(`Rendered ${page.path}!`) + }, + }, + // Optional configuration for specific pages (without this it will still automatically + // prerender all routes) + pages: [ + { + path: '/my-page', + prerender: { enabled: true, outputPath: '/my-page/index.html' }, + }, + ], + }), + viteSolid({ ssr: true }), + ], +}) +``` diff --git a/docs/start/framework/solid/tailwind-integration.md b/docs/start/framework/solid/tailwind-integration.md index edb778d6963..b4b23e57dee 100644 --- a/docs/start/framework/solid/tailwind-integration.md +++ b/docs/start/framework/solid/tailwind-integration.md @@ -1,5 +1,144 @@ --- -ref: docs/start/framework/react/tailwind-integration.md -replace: - { '@tanstack/react-start': '@tanstack/solid-start', 'React': 'SolidJS' } +id: tailwind-integration +title: Tailwind CSS Integration --- + +_So you want to use Tailwind CSS in your TanStack Start project?_ + +This guide will help you use Tailwind CSS in your TanStack Start project. + +## Tailwind CSS Version 4 (Latest) + +The latest version of Tailwind CSS is 4. And it has some configuration changes that majorly differ from Tailwind CSS Version 3. It's **easier and recommended** to set up Tailwind CSS Version 4 in a TanStack Start project, as TanStack Start uses Vite as its build tool. + +### Install Tailwind CSS + +Install Tailwind CSS and it's Vite plugin. + +```shell +npm install tailwindcss @tailwindcss/vite +``` + +### Configure The Vite Plugin + +Add the `@tailwindcss/vite` plugin to your Vite configuration. + +```ts +// vite.config.ts +import { defineConfig } from 'vite' +import tsConfigPaths from 'vite-tsconfig-paths' +import { tanstackStart } from '@tanstack/solid-start/plugin/vite' +import tailwindcss from '@tailwindcss/vite' +import viteSolid from 'vite-plugin-solid' + +export default defineConfig({ + server: { + port: 3000, + }, + plugins: [ + tsConfigPaths(), + tanstackStart(), + viteSolid({ ssr: true }), + tailwindcss(), + ], +}) +``` + +### Import Tailwind in your CSS file + +You need to create a CSS file to configure Tailwind CSS instead of the configuration file in version 4. You can do this by creating a `src/styles/app.css` file or name it whatever you want. + +```css +/* src/styles/app.css */ +@import 'tailwindcss'; +``` + +## Import the CSS file in your `__root.tsx` file + +Import the CSS file in your `__root.tsx` file with the `?url` query and make sure to add the **triple slash** directive to the top of the file. + +```tsx +// src/routes/__root.tsx +/// +// other imports... + +import appCss from '../styles/app.css?url' + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + // your meta tags and site config + ], + links: [{ rel: 'stylesheet', href: appCss }], + // other head config + }), + component: RootComponent, +}) +``` + +## Use Tailwind CSS anywhere in your project + +You can now use Tailwind CSS anywhere in your project. + +```tsx +// src/routes/index.tsx +import { createFileRoute } from '@tanstack/solid-router' + +export const Route = createFileRoute('/')({ + component: Home, +}) + +function Home() { + return
Hello World
+} +``` + +That's it! You can now use Tailwind CSS anywhere in your project 🎉. + +## Tailwind CSS Version 3 (Legacy) + +If you are want to use Tailwind CSS Version 3, you can use the following steps. + +### Install Tailwind CSS + +Install Tailwind CSS and it's peer dependencies. + +```shell +npm install -D tailwindcss@3 postcss autoprefixer +``` + +Then generate the Tailwind and PostCSS configuration files. + +```shell +npx tailwindcss init -p +``` + +### Configure your template paths + +Add the paths to all of your template files in the `tailwind.config.js` file. + +```js +// tailwind.config.js +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: {}, + }, + plugins: [], +} +``` + +### Add the Tailwind directives to your CSS file + +Add the `@tailwind` directives for each of Tailwind's layers to your `src/styles/app.css` file. + +```css +/* src/styles/app.css */ +@tailwind base; +@tailwind components; +@tailwind utilities; +``` + +> [!NOTE] +> Jump to [Import the CSS file in your `__root.tsx` file](#import-the-css-file-in-your-__roottsx-file) to see how to import the CSS file in your `__root.tsx` file.