Published: August 04 2021

Next.js 11 - JWT Authentication Tutorial with Example App

Tutorial built with Next.js 11.0.1

Other versions available:

This tutorial shows how to build a simple full stack login application in Next.js that uses JWT authentication.

Example Next.js Client App

The Next.js client (React) app contains two pages:

  • /login - public login page with username and password fields, on submit the page sends a POST request to the API to authenticate user credentials, on success the API returns a JWT token to make authenticated requests to secure api routes.
  • / - secure home page that displays a list of users fetched from a secure API endpoint using the JWT token received after successful login.

Example Next.js API

The Next.js API contains two routes/endpoints:

  • /api/users/authenticate - public route for authenticating username and password, on success returns a JWT token and basic user details.
  • /api/users - secure route that returns all users for requests contain a valid JWT token.

User data is stored in a JSON flat file (/data/users.json) to keep the example simple and focused on authentication. In a production application data should be stored in a database (e.g. MySQL, MongoDB, PostgreSQL etc).

React Hook Form Library

The login form in the example is built with React Hook Form - a relatively new library for working with forms in React using React Hooks, I stumbled across it last year and have been using it in my React and Next.js projects since then, I think it's easier to use than the other options available and requires less code. For more info see

Code on GitHub

The example project is available on GitHub at

Here it is in action: (See on CodeSandbox at

Run the Next.js JWT Authentication Example Locally

  1. Install Node.js and npm from
  2. Download or clone the Next.js project source code from
  3. Install all required npm packages by running npm install or npm i from the command line in the project root folder (where the package.json is located).
  4. Start the app by running npm run dev from the command line in the project root folder, this will compile the app and start the Next.js server.
  5. Open to the app at the URL http://localhost:3000.

NOTE: You can also start the JWT auth app directly with the Next.js CLI command npx next dev. For more info on the Next.js CLI see

Next.js Project Structure

The project is organised into the following folders:

  • components
    React components used by pages or by other react components.
  • data
    JSON flat files for storing the example JWT auth app data.
  • helpers
    Anything that doesn't fit into the other folders and doesn't justify having its own folder. Front-end helpers are in the root /helpers folder and API helpers are in the /helpers/api subfolder.
  • pages
    Pages and API route handlers for the Next.js JWT auth app. The /pages folder contains all routed pages with the route to each page defined by its file name. The /pages/api folder contains all API route handlers which are also routed based on each file name. For more info on Next.js Page Routing and file name patterns see, for API Routing see
  • services
    Services handle all HTTP communication with backend APIs for the React front-end application, each service encapsulates the API calls for a content type (e.g. users) and exposes methods for performing various operations (e.g. authentication and CRUD operations). Services can also perform actions that don't involve HTTP requests, such as logging out and redirecting to the login page.
  • styles
    CSS stylesheets used by the example JWT auth app.

JavaScript files are organised with export statements at the top so it's easy to see all exported modules when you open a file. Export statements are followed by functions and other implementation code for each JS module.

The index.js files in some folders (components, helpers, services) re-export all of the exports from the folder so they can be imported using only the folder path instead of the full path to each file, and to enable importing multiple modules in a single import (e.g. import { errorHandler, jwtMiddleware } from 'helpers/api').

The baseUrl is set to "." in the jsconfig.json file to make all import statements (without a dot '.' prefix) relative to the root folder of the project, removing the need for long relative paths like import { userService } from '../../../services';.

Click any of the below links to jump down to a description of each file along with it's code:


Link Component

Path: /components/Link.jsx

A custom link component that wraps the Next.js link component to make it work more like the standard link component from React Router.

The built-in Next.js link component accepts an href attribute but requires an <a> tag to be nested inside it to work. Attributes other than href (e.g. className) must be added to the <a> tag. For more info on the Next.js link component see

This custom link component accepts href, className plus any other props, and doesn't require any nested <a> tag (e.g. <Link href="/" className="my-class">Home</Link>).

import NextLink from 'next/link';

export { Link };

function Link({ href, children, ...props }) {
    return (
        <NextLink href={href}>
            <a {...props}>

Nav Component

Path: /components/Nav.jsx

The nav component displays the main navigation in the example. The custom NavLink component automatically adds the active class to the active nav item so it is highlighted in the UI.

import { useState, useEffect } from 'react';

import { NavLink } from '.';
import { userService } from 'services';

export { Nav };

function Nav() {
    const [user, setUser] = useState(null);

    useEffect(() => {
        const subscription = userService.user.subscribe(x => setUser(x));
        return () => subscription.unsubscribe();
    }, []);

    function logout() {

    // only show nav when logged in
    if (!user) return null;
    return (
        <nav className="navbar navbar-expand navbar-dark bg-dark">
            <div className="navbar-nav">
                <NavLink href="/" exact className="nav-item nav-link">Home</NavLink>
                <a onClick={logout} className="nav-item nav-link">Logout</a>

NavLink Component

Path: /components/NavLink.jsx

An extended version of the custom link component that adds the CSS className "active" when the href matches the current URL. By default the href only needs to match the start of the URL, use the exact property to change it to an exact match (e.g. <NavLink href="/" exact>Home</NavLink>).

import { useRouter } from 'next/router';
import PropTypes from 'prop-types';

import { Link } from '.';

export { NavLink };

NavLink.propTypes = {
    href: PropTypes.string.isRequired,
    exact: PropTypes.bool

NavLink.defaultProps = {
    exact: false

function NavLink({ children, href, exact, ...props }) {
    const { pathname } = useRouter();
    const isActive = exact ? pathname === href : pathname.startsWith(href);
    if (isActive) {
        props.className += ' active';

    return <Link href={href} {...props}>{children}</Link>;

Users JSON Data File

Path: /data/users.json

A JSON file containing user data for the example Next.js API, the data is accessed by the users api route handlers located in the /pages/api/users folder.

I decided to use a JSON file to store data instead of a database (e.g. MySQL, MongoDB, PostgreSQL etc) to keep the example simple and focused on the implementation of JWT authentication in Next.js.

        "id": 1,
        "username": "test",
        "password": "test",
        "firstName": "Test",
        "lastName": "User"

Next.js API Handler

Path: /helpers/api/api-handler.js

The API handler is a wrapper function for all API route handlers in the /pages/api folder. It enables adding global middleware to the Next.js request pipeline, and adds support for a global error handler. It is used by the authenticate handler and users index handler.

import { errorHandler, jwtMiddleware } from 'helpers/api';

export { apiHandler };

function apiHandler(handler) {
    return async (req, res) => {
        try {
            // global middleware
            await jwtMiddleware(req, res);

            // route handler
            await handler(req, res);
        } catch (err) {
            // global error handler
            errorHandler(err, res);

Next.js Global Error Handler

Path: /helpers/api/error-handler.js

The global error handler is used catch all errors and remove the need for duplicated error handling code throughout the Next.js JWT auth api. It's added to the request pipeline in the API handler wrapper function.

By convention errors of type 'string' are treated as custom (app specific) errors, this simplifies the code for throwing custom errors since only a string needs to be thrown (e.g. throw 'Username or password is incorrect') which returns a HTTP 400 response code with the thrown error message. If the error name is 'UnauthorizedError' it means JWT token validation has failed so a HTTP 401 response code is returned with the message 'Invalid Token'. Otherwise the default error response code is 500.

export { errorHandler };

function errorHandler(err, res) {
    if (typeof (err) === 'string') {
        // custom application error
        return res.status(400).json({ message: err });

    if ( === 'UnauthorizedError') {
        // jwt authentication error
        return res.status(401).json({ message: 'Invalid Token' });

    // default to 500 server error
    return res.status(500).json({ message: err.message });

Next.js JWT Middleware

Path: /helpers/api/jwt-middleware.js

The JWT middleware uses the express-jwt library to validate JWT tokens sent to protected API routes, if a token is invalid an error is thrown which causes the global error handler to return a 401 Unauthorized response. The middleware is added to the Next.js request pipeline in the API handler wrapper function.

const expressJwt = require('express-jwt');
const util = require('util');
import getConfig from 'next/config';

const { serverRuntimeConfig } = getConfig();

export { jwtMiddleware };

function jwtMiddleware(req, res) {
    const middleware = expressJwt({ secret: serverRuntimeConfig.secret, algorithms: ['HS256'] }).unless({
        path: [
            // public routes that don't require authentication

    return util.promisify(middleware)(req, res);

Fetch Wrapper

Path: /helpers/fetch-wrapper.js

The fetch wrapper is a lightweight wrapper around the native browser fetch() function used to simplify the code for making HTTP requests. It contains methods for get, post, put and delete requests, it automatically handles the parsing of JSON data from responses, and throws an error if the HTTP response is not successful (!response.ok). If the response is 401 Unauthorized or 403 Forbidden the user is automatically logged out.

The authHeader() function is used to automatically add a JWT auth token to the HTTP Authorization header of the request if the user is logged in and the request is to the application api url.

With the fetch wrapper a POST request can be made as simply as this:, body);. It's used in the example app by the user service. For more info see Fetch API - A Lightweight Fetch Wrapper to Simplify HTTP Requests.

import getConfig from 'next/config';

import { userService } from 'services';

const { publicRuntimeConfig } = getConfig();

export const fetchWrapper = {
    delete: _delete

function get(url) {
    const requestOptions = {
        method: 'GET',
        headers: authHeader(url)
    return fetch(url, requestOptions).then(handleResponse);

function post(url, body) {
    const requestOptions = {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...authHeader(url) },
        credentials: 'include',
        body: JSON.stringify(body)
    return fetch(url, requestOptions).then(handleResponse);

function put(url, body) {
    const requestOptions = {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json', ...authHeader(url) },
        body: JSON.stringify(body)
    return fetch(url, requestOptions).then(handleResponse);    

// prefixed with underscored because delete is a reserved word in javascript
function _delete(url) {
    const requestOptions = {
        method: 'DELETE',
        headers: authHeader(url)
    return fetch(url, requestOptions).then(handleResponse);

// helper functions

function authHeader(url) {
    // return auth header with jwt if user is logged in and request is to the api url
    const user = userService.userValue;
    const isLoggedIn = user && user.token;
    const isApiUrl = url.startsWith(publicRuntimeConfig.apiUrl);
    if (isLoggedIn && isApiUrl) {
        return { Authorization: `Bearer ${user.token}` };
    } else {
        return {};

function handleResponse(response) {
    return response.text().then(text => {
        const data = text && JSON.parse(text);
        if (!response.ok) {
            if ([401, 403].includes(response.status) && userService.userValue) {
                // auto logout if 401 Unauthorized or 403 Forbidden response returned from api

            const error = (data && data.message) || response.statusText;
            return Promise.reject(error);

        return data;

Authenticate API Route Handler

Path: /pages/api/users/authenticate.js

The authenticate handler receives HTTP requests sent to the authenticate route /api/users/authenticate. It supports HTTP POST requests containing a username and password which are authenticated by the authenticate() method.

On successful authentication a JWT (JSON Web Token) is generated with the jsonwebtoken npm package, the token is digitally signed using the secret key stored in next.config.js so it can't be tampered with. The JWT token is returned to the client application which must include it in the HTTP Authorization header of subsequent requests to secure routes, this is handled by the fetch wrapper in the example app.

Users are stored in the users.json file with plain text passwords in the example for simplicity and to keep the tutorial focused on JWT authentication, but in a production application it is recommended to store user records in a database with hashed passwords.

const jwt = require('jsonwebtoken');
import getConfig from 'next/config';

import { apiHandler } from 'helpers/api';

const { serverRuntimeConfig } = getConfig();

// users in JSON file for simplicity, store in a db for production applications
const users = require('data/users.json');

export default apiHandler(handler);

function handler(req, res) {
    switch (req.method) {
        case 'POST':
            return authenticate();
            return res.status(405).end(`Method ${req.method} Not Allowed`)

    function authenticate() {
        const { username, password } = req.body;
        const user = users.find(u => u.username === username && u.password === password);

        if (!user) throw 'Username or password is incorrect';
        // create a jwt token that is valid for 7 days
        const token = jwt.sign({ sub: }, serverRuntimeConfig.secret, { expiresIn: '7d' });
        // return basic user details and token
        return res.status(200).json({
            username: user.username,
            firstName: user.firstName,
            lastName: user.lastName,

Users Index API Route Handler

Path: /pages/api/users/index.js

The users index handler receives HTTP requests sent to the base users route /api/users. It supports HTTP GET requests which are mapped to the getUsers() method, which returns all users without their password property. Security for this and all other routes in the API is handled by the global JWT middleware.

import { apiHandler } from 'helpers/api';

// users in JSON file for simplicity, store in a db for production applications
const users = require('data/users.json');

export default apiHandler(handler);

function handler(req, res) {
    switch (req.method) {
        case 'GET':
            return getUsers();
            return res.status(405).end(`Method ${req.method} Not Allowed`)

    function getUsers() {
        // return users without passwords in the response
        const response = => {
            const { password, ...userWithoutPassword } = user;
            return userWithoutPassword;
        return res.status(200).json(response);

Next.js App Component

Path: /pages/_app.js

The App component is the root component of the example Next.js app, it contains the outer html, main nav, and the component for the current page.

Client-side authorization is implemented in the authCheck() function which is executed on initial app load and on each route change. If you try to access a secure page (e.g. the home page /) without logging in, the page contents won't be displayed and you'll be redirected to the /login page. The authorized state property is used to prevent the brief display of secure pages before the redirect because I couldn't find a clean way to cancel a route change using the Next.js routeChangeStart event and then redirecting to a new page.

The Next.js Head component is used to set the default <title> in the html <head> element and add the bootstrap css stylesheet. For more info on the Next.js head component see

The App component overrides the default Next.js App component because it's in a file named /pages/_app.js and supports several features, for more info see

import Head from 'next/head';
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';

import 'styles/globals.css';
import { userService } from 'services';
import { Nav } from 'components';

export default App;

function App({ Component, pageProps }) {
    const router = useRouter();
    const [authorized, setAuthorized] = useState(false);

    useEffect(() => {
        // run auth check on initial load

        // set authorized to false to hide page content while changing routes
        const hideContent = () => setAuthorized(false);'routeChangeStart', hideContent);

        // run auth check on route change'routeChangeComplete', authCheck)

        // unsubscribe from events in useEffect return function
        return () => {
  'routeChangeStart', hideContent);
  'routeChangeComplete', authCheck);
    }, []);

    function authCheck(url) {
        // redirect to login page if accessing a private page and not logged in 
        const publicPaths = ['/login'];
        const path = url.split('?')[0];
        if (!userService.userValue && !publicPaths.includes(path)) {
                pathname: '/login',
                query: { returnUrl: router.asPath }
        } else {

    return (
                <title>Next.js 11 - JWT Authentication Example</title>

                {/* bootstrap css */}
                <link href="//" rel="stylesheet" />

            <div className="app-container bg-light">
                <Nav />
                <div className="container pt-4 pb-4">
                    {authorized &&
                        <Component {...pageProps} />

Home Page

Path: /pages/index.jsx

The home page is a basic react function component that displays some HTML and a list of all users. The component fetches all users from the api on component load by calling the userService.getAll() method from a useEffect() hook.

import { useState, useEffect } from 'react';

import { userService } from 'services';

export default Home;

function Home() {
    const [users, setUsers] = useState(null);

    useEffect(() => {
        userService.getAll().then(x => setUsers(x));
    }, []);

    return (
        <div className="card mt-4">
            <h4 className="card-header">You're logged in with Next.js 11 & JWT!!</h4>
            <div className="card-body">
                <h6>Users from secure api end point</h6>
                {users &&
                        { =>
                            <li key={}>{user.firstName} {user.lastName}</li>
                {!users && <div className="spinner-border spinner-border-sm"></div>}

Login Page

Path: /pages/login.jsx

The login page contains a form built with the React Hook Form library that contains username and password fields for logging into the Next.js app.

Form validation rules are defined with the Yup schema validation library and passed with the formOptions to the React Hook Form useForm() function, for more info on Yup see

The useForm() hook function returns an object with methods for working with a form including registering inputs, handling form submit, accessing form state, displaying errors and more, for a complete list see

The onSubmit function gets called when the form is submitted and valid, and submits the user credentials to the api by calling userService.login().

The returned JSX template contains the markup for page including the form, input fields and validation messages. The form fields are registered with the React Hook Form by calling the register function with the field name from each input element (e.g. {...register('title')}). For more info on form validation with React Hook Form see React Hook Form 7 - Form Validation Example.

import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';

import { userService } from 'services';

export default Login;

function Login() {
    const router = useRouter();

    useEffect(() => {
        // redirect to home if already logged in
        if (userService.userValue) {
    }, []);

    // form validation rules 
    const validationSchema = Yup.object().shape({
        username: Yup.string().required('Username is required'),
        password: Yup.string().required('Password is required')
    const formOptions = { resolver: yupResolver(validationSchema) };

    // get functions to build form with useForm() hook
    const { register, handleSubmit, setError, formState } = useForm(formOptions);
    const { errors } = formState;

    function onSubmit({ username, password }) {
        return userService.login(username, password)
            .then(() => {
                // get return url from query parameters or default to '/'
                const returnUrl = router.query.returnUrl || '/';
            .catch(error => {
                setError('apiError', { message: error });

    return (
        <div className="col-md-6 offset-md-3 mt-5">
            <div className="alert alert-info">
                Username: test<br />
                Password: test
            <div className="card">
                <h4 className="card-header">Next.js JWT Login Example</h4>
                <div className="card-body">
                    <form onSubmit={handleSubmit(onSubmit)}>
                        <div className="form-group">
                            <input name="username" type="text" {...register('username')} className={`form-control ${errors.username ? 'is-invalid' : ''}`} />
                            <div className="invalid-feedback">{errors.username?.message}</div>
                        <div className="form-group">
                            <input name="password" type="password" {...register('password')} className={`form-control ${errors.password ? 'is-invalid' : ''}`} />
                            <div className="invalid-feedback">{errors.password?.message}</div>
                        <button disabled={formState.isSubmitting} className="btn btn-primary">
                            {formState.isSubmitting && <span className="spinner-border spinner-border-sm mr-1"></span>}
                        {errors.apiError &&
                            <div className="alert alert-danger mt-3 mb-0">{errors.apiError?.message}</div>

User Service

Path: /services/user.service.js

The user service handles communication from the React front-end of the Next.js app to the backend API, it contains methods for logging in and out of the app, and a method for fetching all users from the API. HTTP requests are sent with the help of the fetch wrapper.

On successful login the API returns the user details and a JWT token which are published to all subscribers with the call to, the user object is stored in local storage to stay logged between page refreshes and browser sessions.

RxJS subjects and observables are used by the service to store the current user state and communicate between different components in the application. To learn more about using React with RxJS check out React + RxJS - Communicating Between Components with Observable & Subject.

import { BehaviorSubject } from 'rxjs';
import getConfig from 'next/config';
import Router from 'next/router'

import { fetchWrapper } from 'helpers';

const { publicRuntimeConfig } = getConfig();
const baseUrl = `${publicRuntimeConfig.apiUrl}/users`;
const userSubject = new BehaviorSubject(process.browser && JSON.parse(localStorage.getItem('user')));

export const userService = {
    user: userSubject.asObservable(),
    get userValue () { return userSubject.value },

function login(username, password) {
    return`${baseUrl}/authenticate`, { username, password })
        .then(user => {
            // publish user to subscribers and store in local storage to stay logged in between page refreshes
            localStorage.setItem('user', JSON.stringify(user));

            return user;

function logout() {
    // remove user from local storage, publish null to user subscribers and redirect to login page

function getAll() {
    return fetchWrapper.get(baseUrl);

Global CSS Styles

Path: /styles/globals.css

The globals.css file contains global custom CSS styles for the example JWT auth app. It's imported into the application by the Next.js app component.

a { cursor: pointer; }

.app-container {
    min-height: 350px;

JavaScript Config

Path: /jsconfig.json

The jsconfig baseUrl option is used to configure absolute imports for the Next.js app. Setting the base url to "." makes all javascript import statements (without a dot '.' prefix) relative to the root folder of the project, removing the need for long relative paths like import { userService } from '../../../services';.

Next.js supports absolute imports and module path aliases in the jsconfig file, for more info see

    "compilerOptions": {
        // make all imports without a dot '.' prefix relative to the base url
        "baseUrl": "."

Next.js Config

Path: /next.config.js

The Next.js config file defines global config variables that are available to components in the Next.js app. It supports setting different values for variables based on environment (e.g. development vs production).

serverRuntimeConfig variables are only available to the API on the server side, while publicRuntimeConfig variables are available to the API and the client React app.

The apiUrl is used by the user service to send HTTP requests to the API.

IMPORTANT: The secret property is used to sign and verify JWT tokens for authentication, change it with your own random string to ensure nobody else can generate a JWT with the same secret to gain unauthorized access to your api. A quick and easy way is join a couple of GUIDs together to make a long random string (e.g. from

module.exports = {
    serverRuntimeConfig: {
    publicRuntimeConfig: {
        apiUrl: process.env.NODE_ENV === 'development'
            ? 'http://localhost:3000/api' // development api
            : 'http://localhost:3000/api' // production api


Path: /package.json

The package.json file contains project configuration information including scripts for running and building the Next.js app, and dependencies that get installed when you run npm install or npm i. Full documentation is available on the npm docs website.

For more info on the Next.js CLI commands used in the package.json scripts see

    "name": "next-js-jwt-authentication-example",
    "version": "0.1.0",
    "private": true,
    "scripts": {
        "dev": "next dev",
        "build": "next build",
        "start": "next start"
    "dependencies": {
        "@hookform/resolvers": "^2.6.1",
        "express-jwt": "^6.0.0",
        "jsonwebtoken": "^8.5.1",
        "next": "^11.0.1",
        "prop-types": "^15.7.2",
        "react": "^17.0.2",
        "react-dom": "^17.0.2",
        "react-hook-form": "^7.12.1",
        "rxjs": "^7.3.0",
        "yup": "^0.32.9"

Need Some NextJS Help?

Search fiverr for freelance NextJS developers.

Follow me for updates

On Twitter or RSS.

When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Supported by