Skip to main content

Overview

Laravel Breeze Next.js uses the Next.js 16 App Router with route groups to separate authenticated and guest routes.

Route Structure

src/app/
├── (authenticated)/        # Protected routes
│   ├── layout.tsx         # Auth layout with navigation
│   └── dashboard/
│       └── page.tsx       # /dashboard
├── (guest)/               # Public routes
│   ├── login/
│   │   └── page.tsx       # /login
│   ├── register/
│   │   └── page.tsx       # /register
│   ├── forgot-password/
│   │   └── page.tsx       # /forgot-password
│   ├── password-reset/
│   │   └── [token]/
│   │       └── page.tsx   # /password-reset/[token]
│   └── verify-email/
│       └── page.tsx       # /verify-email
├── layout.tsx             # Root layout
├── page.tsx               # Home page
└── not-found.tsx          # 404 page

Route Groups

Route groups organize routes without affecting the URL structure. They’re created using parentheses: (groupName).

Why Route Groups?

  • Separation of concerns - Keep authenticated and guest routes separate
  • Shared layouts - Apply different layouts to different route groups
  • Middleware logic - Handle authentication at the layout level
  • No URL impact - The parentheses don’t appear in the URL

Root Layout

The root layout wraps all pages and sets up the HTML structure:
src/app/layout.tsx
import type { Metadata } from 'next'
import { Nunito } from 'next/font/google'
import './globals.css'

const nunito = Nunito({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body className={`${nunito.className} text-gray-900 antialiased`}>
        {children}
      </body>
    </html>
  )
}

Authenticated Routes

Authenticated Layout

The (authenticated) group has its own layout that checks authentication and displays navigation:
src/app/(authenticated)/layout.tsx
'use client'
import { ReactNode } from 'react'
import { useAuth } from '@/hooks/auth'
import Navigation from '@/components/Layouts/Navigation'

const AppLayout = ({ children }: { children: ReactNode }) => {
  const { user } = useAuth({ middleware: 'auth' })

  return (
    <div className="min-h-screen bg-gray-100">
      <Navigation user={user} />

      {/* Page Content */}
      <main>{children}</main>
    </div>
  )
}

export default AppLayout
Key features:
  • Uses 'use client' directive for client-side rendering
  • Calls useAuth with middleware: 'auth' to protect routes
  • Renders the Navigation component with user data
  • Wraps page content in a styled container

Dashboard Page

Example of an authenticated page:
src/app/(authenticated)/dashboard/page.tsx
import React from 'react'

const DashboardPage = () => {
  return (
    <div className="py-12">
      <div className="max-w-7xl mx-auto sm:px-6 lg:px-8">
        <div className="bg-white overflow-hidden shadow-xs sm:rounded-lg">
          <div className="p-6 bg-white border-b border-gray-200">
            {`You're logged in!`}
          </div>
        </div>
      </div>
    </div>
  )
}

export default DashboardPage

Guest Routes

Guest routes don’t have a shared layout but use the AuthCard component for consistent styling.

Login Page

src/app/(guest)/login/page.tsx
'use client'
import Link from 'next/link'
import * as Yup from 'yup'
import { useSearchParams } from 'next/navigation'
import { ErrorMessage, Field, Form, Formik, FormikHelpers } from 'formik'

import { useAuth } from '@/hooks/auth'
import ApplicationLogo from '@/components/ApplicationLogo'
import AuthCard from '@/components/AuthCard'
import AuthSessionStatus from '@/components/AuthSessionStatus'

interface Values {
  email: string
  password: string
  remember: boolean
}

const LoginPage = () => {
  const searchParams = useSearchParams()
  const [status, setStatus] = useState<string>('')

  const { login } = useAuth({
    middleware: 'guest',
    redirectIfAuthenticated: '/dashboard',
  })

  const submitForm = async (
    values: Values,
    { setSubmitting, setErrors }: FormikHelpers<Values>,
  ): Promise<any> => {
    try {
      await login(values)
    } catch (error: Error | AxiosError | any) {
      if (axios.isAxiosError(error) && error.response?.status === 422) {
        setErrors(error.response?.data?.errors)
      }
    } finally {
      setSubmitting(false)
      setStatus('')
    }
  }

  const LoginSchema = Yup.object().shape({
    email: Yup.string()
      .email('Invalid email')
      .required('The email field is required.'),
    password: Yup.string().required('The password field is required.'),
  })

  return (
    <AuthCard
      logo={
        <Link href="/">
          <ApplicationLogo className="w-20 h-20 fill-current text-gray-500" />
        </Link>
      }>
      <AuthSessionStatus className="mb-4" status={status} />

      <Formik
        onSubmit={submitForm}
        validationSchema={LoginSchema}
        initialValues={{ email: '', password: '', remember: false }}>
        <Form className="space-y-4">
          {/* Form fields */}
        </Form>
      </Formik>
    </AuthCard>
  )
}

export default LoginPage
Key features:
  • Uses middleware: 'guest' to redirect authenticated users
  • redirectIfAuthenticated: '/dashboard' specifies where to redirect
  • Formik handles form state and validation
  • Yup provides schema validation

Register Page

Similar structure to login, with additional fields:
src/app/(guest)/register/page.tsx
interface Values {
  name: string
  email: string
  password: string
  password_confirmation: string
}

const RegisterPage = () => {
  const { register } = useAuth({
    middleware: 'guest',
    redirectIfAuthenticated: '/dashboard',
  })

  const RegisterSchema = Yup.object().shape({
    name: Yup.string().required('The name field is required.'),
    email: Yup.string()
      .email('Invalid email')
      .required('The email field is required.'),
    password: Yup.string().required('The password field is required.'),
    password_confirmation: Yup.string()
      .required('Please confirm password.')
      .oneOf([Yup.ref('password')], 'Your passwords do not match.'),
  })

  // ... rest of component
}

Dynamic Routes

Password Reset with Token

The password reset route uses dynamic segments:
src/app/(guest)/password-reset/[token]/page.tsx
Access the token in your component:
import { useParams } from 'next/navigation'

const PasswordResetPage = () => {
  const params = useParams()
  const token = params.token // Access the [token] segment
  
  // Use token in password reset request
}
import Link from 'next/link'

<Link href="/dashboard">
  Dashboard
</Link>

<Link href="/login" className="underline text-sm">
  Already registered?
</Link>

Programmatic Navigation

import { useRouter } from 'next/navigation'

const Component = () => {
  const router = useRouter()
  
  const handleSuccess = () => {
    router.push('/dashboard')
  }
  
  return <button onClick={handleSuccess}>Continue</button>
}

Reading URL Parameters

Search Params

import { useSearchParams } from 'next/navigation'

const LoginPage = () => {
  const searchParams = useSearchParams()
  const resetToken = searchParams.get('reset') // ?reset=token
  
  // Use the parameter
}

Dynamic Segments

import { useParams } from 'next/navigation'

const PasswordResetPage = () => {
  const params = useParams()
  const token = params.token // /password-reset/[token]
}

Current Pathname

Check the current route for active states:
import { usePathname } from 'next/navigation'

const Navigation = () => {
  const pathname = usePathname()
  
  return (
    <NavLink 
      href="/dashboard" 
      active={pathname === '/dashboard'}
    >
      Dashboard
    </NavLink>
  )
}

Client vs Server Components

When to Use ‘use client’

Use the 'use client' directive when your component:
  • Uses React hooks (useState, useEffect, etc.)
  • Handles browser events (onClick, onChange, etc.)
  • Uses browser APIs (localStorage, window, etc.)
  • Uses Next.js client hooks (useRouter, useParams, etc.)
'use client'
import { useState } from 'react'
import { useAuth } from '@/hooks/auth'

const LoginPage = () => {
  const [status, setStatus] = useState('')
  const { login } = useAuth({})
  // ...
}

Server Components (Default)

Components without 'use client' are Server Components by default. They:
  • Run on the server
  • Can directly access backend resources
  • Have a smaller JavaScript bundle
  • Cannot use hooks or browser APIs

Best Practices

  1. Use route groups to organize related routes
  2. Share layouts between routes in the same group
  3. Client components only when necessary for interactivity
  4. Type your components with TypeScript interfaces
  5. Validate forms with Yup schemas
  6. Handle errors gracefully with try-catch blocks

Next Steps

Build docs developers (and LLMs) love