Skip to main content

Overview

Laravel Breeze Next.js uses Axios for HTTP requests and SWR for data fetching with caching and revalidation.

Axios Configuration

The Axios instance is configured with Laravel Sanctum support:
src/lib/axios.ts
import Axios, { AxiosInstance } from 'axios'

const axios: AxiosInstance = Axios.create({
  baseURL: process.env.NEXT_PUBLIC_BACKEND_URL,
  headers: {
    'X-Requested-With': 'XMLHttpRequest',
  },
  withCredentials: true,
  withXSRFToken: true,
})

export default axios

Configuration Options

  • baseURL - The Laravel backend URL from environment variables
  • X-Requested-With: XMLHttpRequest - Identifies the request as an AJAX call
  • withCredentials: true - Sends cookies with cross-origin requests
  • withXSRFToken: true - Automatically includes CSRF token from cookies

Environment Variables

Set your backend URL in .env.local:
.env.local
NEXT_PUBLIC_BACKEND_URL=http://localhost:8000

SWR Data Fetching

Basic Usage

SWR provides automatic caching, revalidation, and error handling:
import useSWR from 'swr'
import axios from '@/lib/axios'

const { data, error, mutate } = useSWR('/api/endpoint', () =>
  axios.get('/api/endpoint').then(res => res.data)
)

if (error) return <div>Failed to load</div>
if (!data) return <div>Loading...</div>

return <div>{data.message}</div>

SWR with useAuth

The useAuth hook uses SWR to fetch the current user:
src/hooks/auth.ts
import useSWR from 'swr'
import axios from '@/lib/axios'

const {
  data: user,
  error,
  mutate,
} = useSWR('/api/user', () =>
  axios
    .get('/api/user')
    .then(res => res.data)
    .catch(error => {
      if (error.response.status !== 409) throw error
      router.push('/verify-email')
    }),
)
Key features:
  • Automatic revalidation on focus/reconnect
  • Caching for better performance
  • mutate() to manually revalidate
  • Error handling with catch()

Making API Calls

GET Requests

import axios from '@/lib/axios'

// Fetch data
const response = await axios.get('/api/posts')
const posts = response.data

// With parameters
const response = await axios.get('/api/posts', {
  params: {
    page: 1,
    limit: 10,
  },
})

POST Requests

import axios from '@/lib/axios'

// Create resource
const response = await axios.post('/api/posts', {
  title: 'New Post',
  content: 'Post content...',
})

// With CSRF protection (for auth endpoints)
const csrf = () => axios.get('/sanctum/csrf-cookie')

await csrf()
const response = await axios.post('/login', {
  email: '[email protected]',
  password: 'password',
})

PUT/PATCH Requests

// Update resource
const response = await axios.put('/api/posts/1', {
  title: 'Updated Title',
})

// Partial update
const response = await axios.patch('/api/posts/1', {
  title: 'Updated Title',
})

DELETE Requests

// Delete resource
const response = await axios.delete('/api/posts/1')

Error Handling

Try-Catch Pattern

import axios, { AxiosError } from 'axios'

try {
  const response = await axios.post('/api/posts', data)
  console.log('Success:', response.data)
} catch (error: Error | AxiosError | any) {
  if (axios.isAxiosError(error)) {
    // Handle Axios errors
    if (error.response?.status === 422) {
      // Validation errors
      console.error('Validation errors:', error.response.data.errors)
    } else if (error.response?.status === 401) {
      // Unauthenticated
      console.error('Not authenticated')
    } else {
      // Other errors
      console.error('Error:', error.response?.data?.message)
    }
  } else {
    // Non-Axios errors
    console.error('Unexpected error:', error)
  }
}

Validation Errors (422)

Laravel returns validation errors in this format:
{
  "message": "The given data was invalid.",
  "errors": {
    "email": [
      "The email field is required."
    ],
    "password": [
      "The password field is required."
    ]
  }
}
Handle with Formik:
import { FormikHelpers } from 'formik'

const submitForm = async (
  values: Values,
  { setSubmitting, setErrors }: FormikHelpers<Values>,
) => {
  try {
    await axios.post('/api/endpoint', values)
  } catch (error: Error | AxiosError | any) {
    if (axios.isAxiosError(error) && error.response?.status === 422) {
      // Set field-level errors
      setErrors(error.response?.data?.errors)
    }
  } finally {
    setSubmitting(false)
  }
}

SWR Error Handling

const { data, error, isLoading } = useSWR('/api/posts', fetcher)

if (error) {
  return <div>Error: {error.message}</div>
}

if (isLoading) {
  return <div>Loading...</div>
}

return <div>{data.map(post => ...)}</div>

Authentication Endpoints

The useAuth hook provides methods for all auth endpoints:

Register

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

await register({
  name: 'John Doe',
  email: '[email protected]',
  password: 'password',
  password_confirmation: 'password',
})
Endpoint: POST /register

Login

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

await login({
  email: '[email protected]',
  password: 'password',
  remember: true,
})
Endpoint: POST /login

Forgot Password

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

const response = await forgotPassword({
  email: '[email protected]',
})

const status = response.data.status
Endpoint: POST /forgot-password

Reset Password

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

await resetPassword({
  email: '[email protected]',
  password: 'newpassword',
  password_confirmation: 'newpassword',
  // token from URL params is automatically included
})
Endpoint: POST /reset-password

Email Verification

const { resendEmailVerification } = useAuth({
  middleware: 'auth',
})

const response = await resendEmailVerification()
const status = response.data.status
Endpoint: POST /email/verification-notification

Logout

const { logout } = useAuth({})

await logout()
Endpoint: POST /logout

Get Current User

const { user } = useAuth({ middleware: 'auth' })

// user is fetched via SWR from GET /api/user
Endpoint: GET /api/user

CSRF Protection

All mutation requests (POST, PUT, PATCH, DELETE) to Laravel require CSRF protection.

Automatic CSRF Token

The Axios config includes withXSRFToken: true, which automatically reads the CSRF token from the XSRF-TOKEN cookie and sends it in the X-XSRF-TOKEN header. Before making authenticated requests, fetch the CSRF cookie:
const csrf = () => axios.get('/sanctum/csrf-cookie')

// Then make your request
await csrf()
await axios.post('/login', credentials)
The useAuth hook does this automatically:
src/hooks/auth.ts
const login = async (data: {
  email: string
  password: string
  remember: boolean
}) => {
  try {
    await csrf() // Fetch CSRF token first
    await axios.post('/login', data)
    mutate() // Revalidate user
  } catch (error) {
    throw error
  }
}

SWR Patterns

Revalidation

Manually revalidate cached data:
const { data, mutate } = useSWR('/api/posts', fetcher)

// After creating a new post
await axios.post('/api/posts', newPost)
mutate() // Revalidate to fetch updated list

Conditional Fetching

Only fetch when certain conditions are met:
const { user } = useAuth({})

// Only fetch if user is authenticated
const { data } = useSWR(
  user ? '/api/user-specific-data' : null,
  fetcher
)

Optimistic Updates

Update UI immediately, then revalidate:
const { data, mutate } = useSWR('/api/posts', fetcher)

const createPost = async (newPost) => {
  // Optimistically update UI
  mutate([...data, newPost], false)
  
  // Send request
  await axios.post('/api/posts', newPost)
  
  // Revalidate
  mutate()
}

Dependent Queries

Fetch data that depends on other data:
const { data: user } = useSWR('/api/user', fetcher)
const { data: posts } = useSWR(
  user ? `/api/users/${user.id}/posts` : null,
  fetcher
)

TypeScript Types

Typing API Responses

interface Post {
  id: number
  title: string
  content: string
  created_at: string
  updated_at: string
}

const { data: posts } = useSWR<Post[]>('/api/posts', () =>
  axios.get<Post[]>('/api/posts').then(res => res.data)
)

Typing Error Responses

import { AxiosError } from 'axios'

interface ValidationErrors {
  message: string
  errors: Record<string, string[]>
}

try {
  await axios.post('/api/posts', data)
} catch (error) {
  if (axios.isAxiosError<ValidationErrors>(error)) {
    const errors = error.response?.data?.errors
    // errors is typed as Record<string, string[]> | undefined
  }
}

Best Practices

  1. Use SWR for GET requests - Automatic caching and revalidation
  2. Use axios directly for mutations - POST, PUT, PATCH, DELETE
  3. Always handle errors - Especially 422 validation errors
  4. Fetch CSRF token - Before authenticated mutations
  5. Call mutate() - After mutations to refresh data
  6. Type your responses - Use TypeScript interfaces
  7. Conditional fetching - Only fetch when data is needed
  8. Optimistic updates - For better UX

Common Patterns

Create Resource

const { mutate } = useSWR('/api/posts', fetcher)

const createPost = async (data: CreatePostData) => {
  try {
    await axios.post('/api/posts', data)
    mutate() // Refresh list
  } catch (error) {
    // Handle error
  }
}

Update Resource

const { mutate } = useSWR(`/api/posts/${id}`, fetcher)

const updatePost = async (data: UpdatePostData) => {
  try {
    await axios.put(`/api/posts/${id}`, data)
    mutate() // Refresh data
  } catch (error) {
    // Handle error
  }
}

Delete Resource

const { mutate } = useSWR('/api/posts', fetcher)

const deletePost = async (id: number) => {
  try {
    await axios.delete(`/api/posts/${id}`)
    mutate() // Refresh list
  } catch (error) {
    // Handle error
  }
}

Pagination

const [page, setPage] = useState(1)

const { data, error } = useSWR(
  `/api/posts?page=${page}`,
  fetcher
)

return (
  <>
    {data?.data.map(post => ...)}
    <button onClick={() => setPage(page + 1)}>Next</button>
  </>
)

Next Steps

Build docs developers (and LLMs) love