Example of a Next.js application that fetches data on the server and consumes a Laravel API using Breeze for authentication.
- Features
- Motivation
- Custom auth cookie... why?
- Custom middlewares
- Requirements
- Installation
- Configuration
- Usage
- Development
- API Testing
- Troubleshooting
- Contributing
- License
- Server-Side Rendering (SSR): Efficiently fetch and render data on the server.
- Laravel Breeze: Authentication system using Laravel Breeze.
- TypeScript: Type-safe code for better developer experience.
- Validations with Zod: Schema-based validation for safer data handling.
- shadcn as UI Library: Modern UI components for building sleek interfaces.
- pnpm: Fast, disk space efficient package manager.
- Turbopack: Blazing fast bundler for development.
- Data Fetching with Search Params and Pagination: Example implementation of data fetching with query parameters and pagination.
- Server Actions: Execute server-side actions seamlessly.
- Paths Revalidations: Dynamically revalidate paths for up-to-date content (needed after mutating records on the same page).
- Next.js Built-in Middleware: Middleware for handling requests efficiently.
- Authenticated User Retrieval: Fetch authenticated user details on both client and server components.
Laravel offers it's own example. However, when I was developing an application using this stack, I noticed several things that weren't compatible with what I wanted to achieve:
- It relies heavily on client-side data fetching. What I wanted was to fetch data on the server, since the application wasn't highly interactive, so I didn't need any complex logic for revalidating information and making it highly available and dynamic.
- All the authentication logic is also handled on the client, which gives a terrible user experience from my perspective, since you as user have to wait for the client to "realize" that you are or not authenticated. This means that the components are rendered and only until then the application decides what to do, instead of deciding what to do first and then rendering the result. I thought I could leverage the Next.js built-in middleware to achieve this. Use the right tools for the right job, or something like that.
- This point may seem not important, but on these times, at least for me it is. The example uses JavaScript instead of TypeScript, and while I understand that the example has its age, it could be improved and converted to TypeScript to also show the benefits of using types, not only for autocompletion, but also for having a better sense of what are you doing. That's what I think is the best about strong typing, that the care you need to have makes you understand the project better.
- As it basically works on the client, it doesn't show the possibilities of using API Route Handlers and server actions, so it also doesn't show the subtleties of Laravel Sanctum that are "magically" handled when making requests from the client. Discovering what were the key factors to make it work server to server instead of client to server was a really interesting process (spoiler alert: the subtleties are on the headers).
The other features are just what I think is a must nowadays, like using Zod and some UI library to make your life easier, as well as pnpm so you don't have to see that huge node_modules
folder that easily scares anyone out.
I mentioned on the features that you can retrieve the info. of the authenticated user on both client and server components, and for this I use a custom auth cookie. The reason? The cookies that Laravel provides by itself are not enough to ensure that the user is authenticated. They are created when you make any request, so there's no guarantee that you're "in" and it isn't as easy as redirecting to the login if the cookie has an invalid value or doesn't exist. Of course you can just make a request to any route that is protected using those cookies and see if it works or not, but that leads us to the other problem.
You don't actually have access to the information of the user because you can't decrypt the cookies set by Laravel because you didn't encrypt them on the first place. That also means that you can't tell if the value of the cookie is valid unless you make a request to the API and see what happens. Basically, your application relies on information that was created somewhere else and that you only know that you have to use, but aren't able to know anything about their validity internally, you have to go to ask somebody else.
Therefore, the only logical conclusion was to create a cookie that was handled internally by Next.js that guaranteed a valid session and could be read by Next.js itself seamlessly. For this purpose, the auth_user
cookie is created using the jose package, which is a lightweight JWT library that works on the edge runtime, so it allows us to use it on the middleware without worrying for having or not the needed APIs to generate these tokens.
This cookie is HttpOnly to (try) to provide a bit of security to the application, so accessing to it from the client requires an intermediate API Route Handler (clever, ha? Well, let me believe it's clever).
Custom middlewares are also used to handle the conversion from camelCase to snake_case and viceversa (a middleware for the requests and a middleware for the responses). A detailed explanation is offered on this article that I wrote a while ago when I was dealing with this discrepancy in naming conventions.
This is just a reference because I created the project using the (almost) latest versions of every technology as of now, but using versions a bit older will probably work too.
- Node.js: v22.x or higher
- pnpm: v9.x or higher
- PHP: v8.x or higher
- Composer: v2.x or higher
- Laravel: v11.x or higher
-
Clone the repository:
git clone https://github.com/carlos-talavera/nextjs-laravel-breeze.git cd nextjs-laravel-breeze/frontend
-
Install dependencies:
pnpm install
-
Set up environment variables: Create a
.env
file in thefrontend
directory with the following content:NEXT_PUBLIC_BACKEND_URL=http://localhost:8000 FRONTEND_URL=http://localhost:3000 JWT_SECRET=super-secret
NEXT_PUBLIC_BACKEND_URL
: The URL of the Laravel backend API. This variable is shared between client and server components, hence theNEXT_PUBLIC
prefix.FRONTEND_URL
: The URL of the frontend application. It is used for theReferer
header on requests so Laravel accepts them.JWT_SECRET
: A secret key used for creating a custom auth cookie using thejose
library, which works on the edge runtime (middleware).
-
Navigate to the backend directory:
cd ../backend
-
Install dependencies:
composer install
-
Set up environment variables: Copy the
.env.example
file to.env
and configure it as needed:cp .env.example .env
The default variables include configurations for the database, application key, and other settings that Laravel requires to run correctly. Refer to the
.env.example
file for detailed information. -
Generate application key:
php artisan key:generate
-
Run migrations:
php artisan migrate
-
Navigate to the frontend directory:
cd frontend
-
Start the development server:
pnpm dev
I don't use Sail, since I find it easier to use my own Docker images (and easily adjust a dev image to a prod image), but to simplify this setup, there's nothing better than just installing PHP and enabling the needed extensions and running the dev server.
-
Navigate to the backend directory:
cd backend
-
Start the development server:
php artisan serve
Now you should have both the frontend and backend servers running locally.
-
Lint the code:
pnpm lint
-
Format the code using Prettier:
pnpm format
- Laravel Pint is used for code formatting in the backend. You can run:
vendor/bin/pint
-
Build the frontend application:
pnpm build
-
Start the application in production mode:
pnpm start
- Run the tests:
php artisan test
A Postman's collection is included to test the API. It uses environment variables, so you must create an environment with a variable called base_url
(e. g. localhost:8000
) that will be used for all the requests. There's a route for obtaining Laravel Sanctum's cookies that sets the cookies and creates another variable called xsrf-token
that will be used on every request. This route must be used before and after logging in and before logging out (at least that's what the experience taught me).
One known issue is that when using Turbopack for development, sometimes accessing to an URL directly results on a context error. To resolve this, the server must be stopped and started again.
Contributions are welcome! Please follow these steps to contribute:
- Fork the repository.
- Create a new branch:
git checkout -b feature/your-feature-name
- Make your changes.
- Commit your changes:
git commit -m 'Add some feature'
- Push to the branch:
git push origin feature/your-feature-name
- Create a pull request.
This project is licensed under the MIT License. See the LICENSE file for details.