BetterStarter logoBetterStarter
Guides

Protected Routes

Create routes that require authentication.

Overview

Protected routes require users to be authenticated before accessing them. BetterStarter uses file-based routing with an underscore convention for protected routes.

Creating a Protected Route

Use the _ prefix in your route file to denote it as protected:

File Structure

src/routes/
  profile/
    __root.tsx        # Layout
    index.tsx         # Public profile list (/profile/)
    $_/               # Protected routes group
      settings.tsx    # /profile/settings (requires auth)
      edit.tsx        # /profile/edit (requires auth)

Route Example

// src/routes/profile/$_/settings.tsx
import { createFileRoute, notFound } from '@tanstack/react-router'
import { authMiddleware } from '@/lib/auth/middleware'

const loader = async (opts) => {
  const user = await getAuthUser(opts)
  if (!user) {
    throw notFound()
  }
  return { user }
}

export const Route = createFileRoute('/profile/$_/settings')({
  loader,
  component: SettingsView,
})

function SettingsView() {
  const { user } = Route.useLoaderData()
  return <div>Settings for {user.name}</div>
}

Accessing User Context

In your route loader or component:

import { Route } from '@tanstack/react-router'

function MyComponent() {
  const context = Route.useMatch()
  const user = context?.context?.user
  
  if (!user) {
    return <div>Not authenticated</div>
  }
  
  return <div>Hello, {user.name}</div>
}

Redirect on Auth Required

For routes that should redirect unauthenticated users:

import { redirect } from '@tanstack/react-router'

const loader = async () => {
  const user = await getCurrentUser()
  if (!user) {
    throw redirect({ to: '/auth/signin' })
  }
  return { user }
}

Pattern: Role-Based Access

const loader = async () => {
  const user = await getCurrentUser()
  if (!user || user.role !== 'admin') {
    throw notFound()
  }
  return { user }
}

Testing Protected Routes

Use the route directly in tests:

test('redirects to signin when not authenticated', async () => {
  const loader = Route.options.loader
  expect(() => loader({ params: {} })).toThrow(redirect)
})

On this page