Published: August 29 2021

Next.js - Basic HTTP Authentication Tutorial with Example App

Tutorial built with Next.js 11.1.0

Other versions available:

This tutorial shows how to build a simple full stack login application in Next.js that uses Basic HTTP 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.
  • / - secure home page that displays a list of users fetched from a secure API endpoint using the basic authentication credentials of the authenticated user, basic auth credentials consist of the Base64 encoded username and password separated by a single colon :.

Example Next.js API

The Next.js API contains two routes/endpoints:

  • /api/users/authenticate - POST - public route for authenticating username and password, on success returns basic user details.
  • /api/users - GET - secure route that returns all users for requests contain a valid basic authentication credentials (Base64 encoded username and password separated by a colon :).

Data saved to JSON files

To keep the example as simple as possible, instead of using a database (e.g. MySQL, MongoDB, PostgreSQL etc) I'm storing data for users in a JSON flat file located at /data/users.json, the data is accessed via the users repo. A real database is recommended for production applications.

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


RxJS subjects and observables are used by the user 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.

Code on GitHub

The example project is available on GitHub at

Here it is in action: (See on CodeSandbox at

Run the Next.js Basic 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 basic 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 basic 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 basic 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 basic 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, basicAuthMiddleware } 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>

Nav Link 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>;

Route Guard Component

Path: /components/RouteGuard.jsx

The route guard component contains the client-side authorization logic for the Next.js app, it wraps the current page component in the Next.js app component.

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.

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

import { userService } from 'services';

export { RouteGuard };

function RouteGuard({ children }) {
    const router = useRouter();
    const [authorized, setAuthorized] = useState(false);

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

        // on route change start - hide page content by setting authorized to false  
        const hideContent = () => setAuthorized(false);'routeChangeStart', hideContent);

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

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

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    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 (authorized && children);

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 basic HTTP 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 (e.g. authenticate handler). It enables adding global middleware to the Next.js request pipeline and adds support for global exception handling. The wrapper function accepts a handler object that contains a method for each HTTP method that is supported by the handler (e.g. get, post, put, delete etc). If a request is received for an unsupported HTTP method a 405 Method Not Allowed response is returned.

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

export { apiHandler };

function apiHandler(handler) {
    return async (req, res) => {
        const method = req.method.toLowerCase();

        // check handler supports HTTP method
        if (!handler[method])
            return res.status(405).end(`Method ${req.method} Not Allowed`);

        try {
            // global middleware
            await basicAuthMiddleware(req, res);

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

Next.js Basic Auth Middleware

Path: /helpers/api/basic-auth-middleware.js

The basic authentication middleware checks that the basic authentication credentials (base64 encoded username & password) received in the http request from the client are valid before allowing access to the API, if the auth credentials are 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.

import { usersRepo } from 'helpers/api';

export { basicAuthMiddleware };

async function basicAuthMiddleware(req, res) {
    // make authenticate path public
    if (req.url === '/api/users/authenticate') {

    // check for basic auth header
    if (!req.headers.authorization || req.headers.authorization.indexOf('Basic ') === -1) {
        throw { status: 401, message: 'Missing Authorization Header' };

    // verify auth credentials
    const base64Credentials = req.headers.authorization.split(' ')[1];
    const credentials = Buffer.from(base64Credentials, 'base64').toString('ascii');
    const [username, password] = credentials.split(':');
    const user = await usersRepo.find(x => x.username === username && x.password === password);
    if (!user) {
        throw { status: 401, message: 'Invalid Authentication Credentials' };

    // attach user to request object
    req.user = user

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 basic 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 is an object with a status property the handler returns the status as the response code with the message property in the body. 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 (err.status) {
        // status code set in error object
        return res.status(err.status).json({ message: err.message });

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

Omit Function

Path: /helpers/api/omit.js

The omit() helper function is used to omit/exclude a key from an object (obj). It's used in the tutorial app to omit the password property from users returned by the api (e.g. users index handler).

The lodash library contains an omit function as well, but I decided to write my own since it's a tiny function and would've felt like overkill to add a whole library for it.

export { omit };

function omit(obj, key) {
    const { [key]: omitted, } = obj;
    return rest;

Users Repo

Path: /helpers/users-repo.js

The users repo encapsulates all access to user data stored in the users JSON data file and exposes a couple of methods for reading the data. It's used on the server-side by the Next.js users API route handlers (authenticate.js, index.js).

Data is stored in a JSON file for simplicity to keep the tutorial simple and focused on how to implement basic HTTP authentication in Next.js. A real database (e.g. MySQL, MongoDB, PostgreSQL etc) is recommended for production applications.

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

export const usersRepo = {
    getAll: () => users,
    find: x => users.find(x)

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 basic auth credentials 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 basic auth credentials if user is logged in and request is to the api url
    const user = userService.userValue;
    const isLoggedIn = user && user.authdata;
    const isApiUrl = url.startsWith(publicRuntimeConfig.apiUrl);
    if (isLoggedIn && isApiUrl) {
        return { Authorization: `Basic ${user.authdata}` };
    } 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 the user details are returned which tells the client application that the username and password are correct, the client app then generates basic auth credentials by Base64 encoding the username and password. Basic auth credentials are included in the HTTP Authorization header of subsequent HTTP requests to secure API routes, the auth header is automatically added 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 basic HTTP authentication, but in a production application it is recommended to store user records in a database with hashed passwords.

import { apiHandler, usersRepo, omit } from 'helpers/api';

export default apiHandler({
    post: authenticate

function authenticate(req, res) {
    const { username, password } = req.body;
    const user = usersRepo.find(x => x.username === username && x.password === password);

    if (!user) throw 'Username or password is incorrect';

    // return basic user details on success
    return res.status(200).json(omit(user, 'password'));

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() function, which returns all users without their password property. Security for this and all other secure routes in the API is handled by the global basic auth middleware.

The route handler supports HTTP GET requests by passing an object with a get() method to the apiHandler() function.

import { apiHandler, usersRepo, omit } from 'helpers/api';

export default apiHandler({
    get: getUsers

function getUsers(req, res) {
    // return users without passwords in the response
    const response = usersRepo.getAll().map(x => omit(x, 'password'));
    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.

The current page component <Component {...pageProps} /> is wrapped in a route guard component (<RouteGuard>) that implements client-side authorization to prevent unauthenticated users from accessing secure pages.

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 'styles/globals.css';
import { Nav, RouteGuard } from 'components';

export default App;

function App({ Component, pageProps }) {
    return (
                <title>Next.js 11 - Basic HTTP Authentication Example</title>

                {/* eslint-disable-next-line @next/next/no-css-tags */}
                <link href="//" rel="stylesheet" />

            <div className="app-container bg-light">
                <Nav />
                <div className="container pt-4 pb-4">
                        <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&apos;re logged in with Next.js 11 & Basic HTTP Authentication!!</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('username')}). 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) {

        // eslint-disable-next-line react-hooks/exhaustive-deps
    }, []);

    // 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 Basic Authentication 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, the service then sets the basic auth credentials (user.authdata) to the Base64 encoded username and password, the user object is then published to all subscribers with the call to, the user 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 with basic auth credentials to subscribers and store in 
            // local storage to stay logged in between page refreshes
            user.authdata = window.btoa(username + ':' + password);
            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 basic 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.

module.exports = {
    reactStrictMode: true,
    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-basic-authentication-example",
    "version": "0.1.0",
    "private": true,
    "scripts": {
        "dev": "next dev",
        "build": "next build",
        "start": "next start",
        "lint": "next lint"
    "dependencies": {
        "@hookform/resolvers": "^2.6.1",
        "next": "^11.1.0",
        "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"
    "devDependencies": {
        "eslint": "7.32.0",
        "eslint-config-next": "11.1.0"

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