Published: August 19 2021
Last updated: March 31 2023

Next.js 11 - User Registration and Login Tutorial with Example App

Tutorial built with Next.js 11.1.0

Other versions available:

In this tutorial we'll go through an example of how to build a simple user registration, login and user management (CRUD) application with Next.js.

Next.js Tutorial Client App

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

  • /account/login - public page for logging into the Next.js app.
  • /account/register - public page for registering a new user account with the app.
  • / - secure home page containing a simple welcome message to the logged in user.
  • /users - secure page displaying a list of all users in the Next.js app, with options to add, edit or delete users.
  • /users/add - secure page for adding a new user.
  • /users/edit/[id] - secure page for editing an existing user.

Secure pages are protected by the authCheck() function of the Next.js App Component which redirects unauthenticated users to the login page.

NOTE: Client-side security is more about UX than real security, it isn't difficult to bypass since all the client code is downloaded to the browser and accessible to the user, but the client code doesn't contain any sensitive data and bypassing it won't give you access to the API which requires a valid JWT token to access a secure route.

Next.js Tutorial API

The Next.js API contains the following routes/endpoints:

  • /api/users/authenticate - POST - public route for authenticating username and password and generating a JWT token.
  • /api/users/register - POST - public route for registering a new user with the Next.js app.
  • /api/users - GET - secure route that returns all users.
  • /api/users/[id] - GET - secure route that returns the user with the specified id.
  • /api/users/[id] - PUT - secure route for updating a user.
  • /api/users/[id] - DELETE - secure route for deleting a user.

Secure routes require a valid JWT token in the HTTP Authorization header of the request.

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 and managed via the users repo which supports all basic CRUD operations.

For instructions on how to connect to MongoDB see Next.js 11 + MongoDB - Connect to Mongo database with Mongoose.

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 https://react-hook-form.com.

RxJS

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 https://github.com/cornflourblue/next-js-11-registration-login-example.

Here it is in action: (See on CodeSandbox at https://codesandbox.io/s/nextjs-11-user-registration-and-login-example-zde4h)


Run the Next.js Login Tutorial Example Locally

  1. Install Node.js and npm from https://nodejs.org.
  2. Download or clone the Next.js project source code from https://github.com/cornflourblue/next-js-11-registration-login-example
  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 https://nextjs.org/docs/api-reference/cli.

Before running in production

Before running in production make sure you update the secret property in the Next.js config file, it is used to sign and verify JWT tokens for authentication, change it to a random string to ensure nobody else can generate a JWT with the same secret and 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 https://www.guidgenerator.com/).


Next.js Tutorial Project Structure

The project is organised into the following folders:

  • components
    React components used by pages or by other React components. Global components are in the root /components folder and feature specific components are in subfolders (e.g. /components/account, /components/users).
  • data
    JSON flat files for storing the Next.js tutorial app data.
  • helpers
    Anything that doesn't fit into the other folders and doesn't justify having its own folder. Front-end React helpers are in the root /helpers folder and back-end API helpers are in the /helpers/api subfolder.
  • pages
    Pages and API route handlers for the Next.js login tutorial 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 https://nextjs.org/docs/routing/introduction, for API Routing see https://nextjs.org/docs/api-routes/introduction.
  • 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. CRUD operations). Services can also perform actions that don't involve http calls, such as displaying and clearing alerts with the alert service.
  • styles
    CSS stylesheets used by the Next.js tutorial 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:

 

Account Layout Component

Path: /components/account/Layout.jsx

The account layout component contains common layout code for all pages in the /pages/account folder, it simply wraps the {children} elements in a div with some bootstrap classes to set the width and alignment of all of the account pages. The Layout component is imported by each account page and used to wrap the returned JSX template (e.g. login page, register page).

The useEffect() react hook is used to automatically redirect the user to the home page if they are already logged in.

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

import { userService } from 'services';

export { Layout };

function Layout({ children }) {
    const router = useRouter();

    useEffect(() => {
        // redirect to home if already logged in
        if (userService.userValue) {
            router.push('/');
        }

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

    return (
        <div className="col-md-6 offset-md-3 mt-5">
            {children}
        </div>
    );
}
 

Users Add/Edit Component

Path: /components/users/AddEdit.jsx

The users AddEdit component is used for both adding and editing users, it contains a form built with the React Hook Form library and is used by the add user page and edit user page.

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 https://github.com/jquense/yup.

The useForm() hook function returns an object with methods for working with a form including registering inputs, handling form submit, resetting the form, accessing form state, displaying errors and more, for a complete list see https://react-hook-form.com/api/useform.

The onSubmit function gets called when the form is submitted and valid, and either creates or updates a user depending on which mode it is in.

The form is in "add mode" when there is no user passed in the component props (props.user), otherwise it is in "edit mode". The variable isAddMode is used to change the form behaviour based on the mode it is in, for example in "add mode" the password field is required, and in "edit mode" (!isAddMode) the user details are assigned to the form default values to pre-populate the form when it loads.

The returned JSX template contains the form with all of the 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('firstName')}). For more info on form validation with React Hook Form see React Hook Form 7 - Form Validation Example.

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

import { Link } from 'components';
import { userService, alertService } from 'services';

export { AddEdit };

function AddEdit(props) {
    const user = props?.user;
    const isAddMode = !user;
    const router = useRouter();
    
    // form validation rules 
    const validationSchema = Yup.object().shape({
        firstName: Yup.string()
            .required('First Name is required'),
        lastName: Yup.string()
            .required('Last Name is required'),
        username: Yup.string()
            .required('Username is required'),
        password: Yup.string()
            .transform(x => x === '' ? undefined : x)
            .concat(isAddMode ? Yup.string().required('Password is required') : null)
            .min(6, 'Password must be at least 6 characters')
    });
    const formOptions = { resolver: yupResolver(validationSchema) };

    // set default form values if in edit mode
    if (!isAddMode) {
        formOptions.defaultValues = props.user;
    }

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

    function onSubmit(data) {
        return isAddMode
            ? createUser(data)
            : updateUser(user.id, data);
    }

    function createUser(data) {
        return userService.register(data)
            .then(() => {
                alertService.success('User added', { keepAfterRouteChange: true });
                router.push('.');
            })
            .catch(alertService.error);
    }

    function updateUser(id, data) {
        return userService.update(id, data)
            .then(() => {
                alertService.success('User updated', { keepAfterRouteChange: true });
                router.push('..');
            })
            .catch(alertService.error);
    }

    return (
        <form onSubmit={handleSubmit(onSubmit)}>
            <div className="form-row">
                <div className="form-group col">
                    <label>First Name</label>
                    <input name="firstName" type="text" {...register('firstName')} className={`form-control ${errors.firstName ? 'is-invalid' : ''}`} />
                    <div className="invalid-feedback">{errors.firstName?.message}</div>
                </div>
                <div className="form-group col">
                    <label>Last Name</label>
                    <input name="lastName" type="text" {...register('lastName')} className={`form-control ${errors.lastName ? 'is-invalid' : ''}`} />
                    <div className="invalid-feedback">{errors.lastName?.message}</div>
                </div>
            </div>
            <div className="form-row">
                <div className="form-group col">
                    <label>Username</label>
                    <input name="username" type="text" {...register('username')} className={`form-control ${errors.username ? 'is-invalid' : ''}`} />
                    <div className="invalid-feedback">{errors.email?.message}</div>
                </div>
                <div className="form-group col">
                    <label>
                        Password
                        {!isAddMode && <em className="ml-1">(Leave blank to keep the same password)</em>}
                    </label>
                    <input name="password" type="password" {...register('password')} className={`form-control ${errors.password ? 'is-invalid' : ''}`} />
                    <div className="invalid-feedback">{errors.password?.message}</div>
                </div>
            </div>
            <div className="form-group">
                <button type="submit" disabled={formState.isSubmitting} className="btn btn-primary mr-2">
                    {formState.isSubmitting && <span className="spinner-border spinner-border-sm mr-1"></span>}
                    Save
                </button>
                <button onClick={() => reset(formOptions.defaultValues)} type="button" disabled={formState.isSubmitting} className="btn btn-secondary">Reset</button>
                <Link href="/users" className="btn btn-link">Cancel</Link>
            </div>
        </form>
    );
}
 

Users Layout Component

Path: /components/users/Layout.jsx

The users layout component contains common layout code for all pages in the /pages/users folder, it simply wraps the {children} elements in a couple of div tags with some bootstrap classes to set the width, padding and alignment of all of the users pages. The Layout component is imported by each users page and used to wrap the returned JSX template (e.g. users index page).

export { Layout };

function Layout({ children }) {
    return (
        <div className="p-4">
            <div className="container">
                {children}
            </div>
        </div>
    );
}
 

Alert Component

Path: /components/Alert.jsx

The alert component controls the adding & removing of bootstrap alerts in the UI, it maintains an array of alerts that are rendered in the template returned by the React component.

The useEffect() hook is used to subscribe to the observable returned from the alertService.onAlert() method, this enables the alert component to be notified whenever an alert message is sent to the alert service and add it to the alerts array for display. Sending an alert with an empty message to the alert service tells the alert component to clear the alerts array. The useEffect() hook is also used to register a route change listener by calling router.events.on('routeChangeStart', clearAlerts); which automatically clears alerts on route changes.

The empty dependency array [] passed as a second parameter to the useEffect() hook causes the react hook to only run once when the component mounts, similar to the componentDidMount() method in a traditional react class component. The function returned from the useEffect() hook cleans up the subscribtions when the component unmounts, similar to the componentWillUnmount() method in a traditional react class component.

The removeAlert() function removes the specified alert object from the array, it allows individual alerts to be closed in the UI.

The cssClasses() function returns corresponding bootstrap alert classes for each alert type, if you're using something other than bootstrap you could change the CSS classes returned to suit your application.

The returned JSX template renders a bootstrap alert message for each alert in the alerts array.

For more info see React Hooks + Bootstrap - Alert Notifications.

import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import PropTypes from 'prop-types';

import { alertService, AlertType } from 'services';

export { Alert };

Alert.propTypes = {
    id: PropTypes.string,
    fade: PropTypes.bool
};

Alert.defaultProps = {
    id: 'default-alert',
    fade: true
};

function Alert({ id, fade }) {
    const router = useRouter();
    const [alerts, setAlerts] = useState([]);

    useEffect(() => {
        // subscribe to new alert notifications
        const subscription = alertService.onAlert(id)
            .subscribe(alert => {
                // clear alerts when an empty alert is received
                if (!alert.message) {
                    setAlerts(alerts => {
                        // filter out alerts without 'keepAfterRouteChange' flag
                        const filteredAlerts = alerts.filter(x => x.keepAfterRouteChange);

                        // set 'keepAfterRouteChange' flag to false on the rest
                        filteredAlerts.forEach(x => delete x.keepAfterRouteChange);
                        return filteredAlerts;
                    });
                } else {
                    // add alert to array
                    setAlerts(alerts => ([...alerts, alert]));

                    // auto close alert if required
                    if (alert.autoClose) {
                        setTimeout(() => removeAlert(alert), 3000);
                    }
                }
            });


        // clear alerts on location change
        const clearAlerts = () => {
            setTimeout(() => alertService.clear(id));
        };
        router.events.on('routeChangeStart', clearAlerts);

        // clean up function that runs when the component unmounts
        return () => {
            // unsubscribe to avoid memory leaks
            subscription.unsubscribe();
            router.events.off('routeChangeStart', clearAlerts);
        };

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

    function removeAlert(alert) {
        if (fade) {
            // fade out alert
            const alertWithFade = { ...alert, fade: true };
            setAlerts(alerts => alerts.map(x => x === alert ? alertWithFade : x));

            // remove alert after faded out
            setTimeout(() => {
                setAlerts(alerts => alerts.filter(x => x !== alertWithFade));
            }, 250);
        } else {
            // remove alert
            setAlerts(alerts => alerts.filter(x => x !== alert));
        }
    };

    function cssClasses(alert) {
        if (!alert) return;

        const classes = ['alert', 'alert-dismissable'];

        const alertTypeClass = {
            [AlertType.Success]: 'alert-success',
            [AlertType.Error]: 'alert-danger',
            [AlertType.Info]: 'alert-info',
            [AlertType.Warning]: 'alert-warning'
        }

        classes.push(alertTypeClass[alert.type]);

        if (alert.fade) {
            classes.push('fade');
        }

        return classes.join(' ');
    }

    if (!alerts.length) return null;

    return (
        <div className="container">
            <div className="m-3">
                {alerts.map((alert, index) =>
                    <div key={index} className={cssClasses(alert)}>
                        <a className="close" onClick={() => removeAlert(alert)}>&times;</a>
                        <span dangerouslySetInnerHTML={{ __html: alert.message }}></span>
                    </div>
                )}
            </div>
        </div>
    );
}
 

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 https://nextjs.org/docs/api-reference/next/link.

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}>
                {children}
            </a>
        </NextLink>
    );
}
 

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() {
        userService.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>
                <NavLink href="/users" className="nav-item nav-link">Users</NavLink>
                <a onClick={logout} className="nav-item nav-link">Logout</a>
            </div>
        </nav>
    );
}
 

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>;
}
 

Spinner Component

Path: /components/Spinner.jsx

A simple bootstrap loading spinner component, used by the users index page and edit user page.

export { Spinner };

function Spinner() {
    return (
        <div className="text-center p-3">
            <span className="spinner-border spinner-border-lg align-center"></span>
        </div>
    );
}
 

Users JSON Data File

Path: /data/users.json

A JSON file containing user data for the Next.js tutorial app, the data is accessed and managed via the users repo which supports all basic CRUD operations. The file contains an empty array ([]) by default which is first populated when a new user is registered.

I decided to use a JSON file to store data instead of a database (e.g. MySQL, MongoDB, PostgreSQL etc) to keep the tutorial simple and focused on how to implement user registration and login functionality in Next.js.

[]
 

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, register 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, jwtMiddleware } 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 jwtMiddleware(req, res);

            // route handler
            await handler[method](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 tutorial 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'), if a custom error ends with the words 'not found' a 404 response code is returned, otherwise a standard 400 error response is returned.

If the error is an object with the name 'UnauthorizedError' it means JWT token validation has failed so a HTTP 401 unauthorized response code is returned with the message 'Invalid Token'.

All other (unhandled) exceptions are logged to the console and return a 500 server error response code.

export { errorHandler };

function errorHandler(err, res) {
    if (typeof (err) === 'string') {
        // custom application error
        const is404 = err.toLowerCase().endsWith('not found');
        const statusCode = is404 ? 404 : 400;
        return res.status(statusCode).json({ message: err });
    }

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

    // default to 500 server error
    console.error(err);
    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 in requests 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.

The register and authenticate routes are made public by passing them to the unless() method of the express-jwt library. For more info on express-jwt see https://www.npmjs.com/package/express-jwt.

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
            '/api/users/register',
            '/api/users/authenticate'
        ]
    });

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

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 hash property from users returned by the api (e.g. users index handler, users id 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, ...rest } = 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 standard set of CRUD methods for reading and managing the data. It's used on the server-side by the Next.js users API route handlers (authenticate.js, register.js, [id].js, index.js).

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

const fs = require('fs');

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

export const usersRepo = {
    getAll: () => users,
    getById: id => users.find(x => x.id.toString() === id.toString()),
    find: x => users.find(x),
    create,
    update,
    delete: _delete
};

function create(user) {
    // generate new user id
    user.id = users.length ? Math.max(...users.map(x => x.id)) + 1 : 1;

    // set date created and updated
    user.dateCreated = new Date().toISOString();
    user.dateUpdated = new Date().toISOString();

    // add and save user
    users.push(user);
    saveData();
}

function update(id, params) {
    const user = users.find(x => x.id.toString() === id.toString());

    // set date updated
    user.dateUpdated = new Date().toISOString();

    // update and save
    Object.assign(user, params);
    saveData();
}

// prefixed with underscore '_' because 'delete' is a reserved word in javascript
function _delete(id) {
    // filter out deleted user and save
    users = users.filter(x => x.id.toString() !== id.toString());
    saveData();
    
}

// private helper functions

function saveData() {
    fs.writeFileSync('data/users.json', JSON.stringify(users, null, 4));
}
 

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 of the Next.js app.

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: fetchWrapper.post(url, 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 = {
    get,
    post,
    put,
    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
                userService.logout();
            }

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

        return data;
    });
}
 

Next.js Login Page

Path: /pages/account/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 tutorial 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 https://github.com/jquense/yup.

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 https://react-hook-form.com/api/useform.

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 { useRouter } from 'next/router';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';

import { Link } from 'components';
import { Layout } from 'components/account';
import { userService, alertService } from 'services';

export default Login;

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

    // 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, 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 || '/';
                router.push(returnUrl);
            })
            .catch(alertService.error);
    }

    return (
        <Layout>
            <div className="card">
                <h4 className="card-header">Login</h4>
                <div className="card-body">
                    <form onSubmit={handleSubmit(onSubmit)}>
                        <div className="form-group">
                            <label>Username</label>
                            <input name="username" type="text" {...register('username')} className={`form-control ${errors.username ? 'is-invalid' : ''}`} />
                            <div className="invalid-feedback">{errors.username?.message}</div>
                        </div>
                        <div className="form-group">
                            <label>Password</label>
                            <input name="password" type="password" {...register('password')} className={`form-control ${errors.password ? 'is-invalid' : ''}`} />
                            <div className="invalid-feedback">{errors.password?.message}</div>
                        </div>
                        <button disabled={formState.isSubmitting} className="btn btn-primary">
                            {formState.isSubmitting && <span className="spinner-border spinner-border-sm mr-1"></span>}
                            Login
                        </button>
                        <Link href="/account/register" className="btn btn-link">Register</Link>
                    </form>
                </div>
            </div>
        </Layout>
    );
}
 

Next.js Register Page

Path: /pages/account/register.jsx

The register page contains a simple registration form built with the React Hook Form library with fields for first name, last name, username and password.

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 https://github.com/jquense/yup.

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 https://react-hook-form.com/api/useform.

The onSubmit function gets called when the form is submitted and valid, and submits the form data to the Next.js api by calling userService.register().

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 { useRouter } from 'next/router';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as Yup from 'yup';

import { Link } from 'components';
import { Layout } from 'components/account';
import { userService, alertService } from 'services';

export default Register;

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

    // form validation rules 
    const validationSchema = Yup.object().shape({
        firstName: Yup.string()
            .required('First Name is required'),
        lastName: Yup.string()
            .required('Last Name is required'),
        username: Yup.string()
            .required('Username is required'),
        password: Yup.string()
            .required('Password is required')
            .min(6, 'Password must be at least 6 characters')
    });
    const formOptions = { resolver: yupResolver(validationSchema) };

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

    function onSubmit(user) {
        return userService.register(user)
            .then(() => {
                alertService.success('Registration successful', { keepAfterRouteChange: true });
                router.push('login');
            })
            .catch(alertService.error);
    }

    return (
        <Layout>
            <div className="card">
                <h4 className="card-header">Register</h4>
                <div className="card-body">
                    <form onSubmit={handleSubmit(onSubmit)}>
                        <div className="form-group">
                            <label>First Name</label>
                            <input name="firstName" type="text" {...register('firstName')} className={`form-control ${errors.firstName ? 'is-invalid' : ''}`} />
                            <div className="invalid-feedback">{errors.firstName?.message}</div>
                        </div>
                        <div className="form-group">
                            <label>Last Name</label>
                            <input name="lastName" type="text" {...register('lastName')} className={`form-control ${errors.lastName ? 'is-invalid' : ''}`} />
                            <div className="invalid-feedback">{errors.lastName?.message}</div>
                        </div>
                        <div className="form-group">
                            <label>Username</label>
                            <input name="username" type="text" {...register('username')} className={`form-control ${errors.username ? 'is-invalid' : ''}`} />
                            <div className="invalid-feedback">{errors.username?.message}</div>
                        </div>
                        <div className="form-group">
                            <label>Password</label>
                            <input name="password" type="password" {...register('password')} className={`form-control ${errors.password ? 'is-invalid' : ''}`} />
                            <div className="invalid-feedback">{errors.password?.message}</div>
                        </div>
                        <button disabled={formState.isSubmitting} className="btn btn-primary">
                            {formState.isSubmitting && <span className="spinner-border spinner-border-sm mr-1"></span>}
                            Register
                        </button>
                        <Link href="/account/login" className="btn btn-link">Cancel</Link>
                    </form>
                </div>
            </div>
        </Layout>
    );
}
 

Users ID API Route Handler

Path: /pages/api/users/[id].js

A dynamic API route handler that handles HTTP requests with any value as the [id] parameter (i.e. /api/users/*). The user id parameter is attached by Next.js to the req.query object and accessible to the route handler.

The route handler supports HTTP GET, PUT and DELETE requests by passing an object with those method names (in lower case) to the apiHandler() function which map to the functions getById(), update() and _delete().

const bcrypt = require('bcryptjs');

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

export default apiHandler({
    get: getById,
    put: update,
    delete: _delete
});

function getById(req, res) {
    const user = usersRepo.getById(req.query.id);

    if (!user) throw 'User Not Found';

    return res.status(200).json(omit(user, 'hash'));
}

function update(req, res) {
    const user = usersRepo.getById(req.query.id);

    if (!user) throw 'User Not Found';

    // split out password from user details 
    const { password, ...params } = req.body;

    // validate
    if (user.username !== params.username && usersRepo.find(x => x.username === params.username))
        throw `User with the username "${params.username}" already exists`;

    // only update hashed password if entered
    if (password) {
        user.hash = bcrypt.hashSync(password, 10);
    }

    usersRepo.update(req.query.id, params);
    return res.status(200).json({});
}

function _delete(req, res) {
    usersRepo.delete(req.query.id);
    return res.status(200).json({});
}
 

Users 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() function.

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

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 tutorial app.

Bcrypt is used to hash and verify passwords in the Next.js tutorial with the bcryptjs library, for more info see Node.js - Hash and Verify Passwords with Bcrypt.

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

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

const { serverRuntimeConfig } = getConfig();

export default apiHandler({
    post: authenticate
});

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

    // validate
    if (!(user && bcrypt.compareSync(password, user.hash))) {
        throw 'Username or password is incorrect';
    }

    // create a jwt token that is valid for 7 days
    const token = jwt.sign({ sub: user.id }, serverRuntimeConfig.secret, { expiresIn: '7d' });

    // return basic user details and token
    return res.status(200).json({
        id: user.id,
        username: user.username,
        firstName: user.firstName,
        lastName: user.lastName,
        token
    });
}
 

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 hash property. Security for this and all other secure routes in the API is handled by the global JWT 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 hashed passwords in the response
    const response = usersRepo.getAll().map(x => omit(x, 'hash'));
    return res.status(200).json(response);
}
 

Users Register API Route Handler

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

The register handler receives HTTP requests sent to the register route /api/users/register. It supports HTTP POST requests containing user details which are registered in the Next.js tutorial app by the register() function. On successful registration a 200 OK response is returned with an empty JSON object.

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

Bcrypt is used to hash and verify passwords in the Next.js tutorial with the bcryptjs library, for more info see Node.js - Hash and Verify Passwords with Bcrypt.

const bcrypt = require('bcryptjs');

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

export default apiHandler({
    post: register
});

function register(req, res) {
    // split out password from user details 
    const { password, ...user } = req.body;

    // validate
    if (usersRepo.find(x => x.username === user.username))
        throw `User with the username "${user.username}" already exists`;

    // hash password
    user.hash = bcrypt.hashSync(password, 10);    

    usersRepo.create(user);
    return res.status(200).json({});
}
 

Edit User Page

Path: /pages/users/edit/[id].jsx

The edit user page wraps the add/edit user component and passes it the specified user to set it to "edit" mode. The user is fetched from the API in a useEffect() React hook with the id parameter from the URL, the id is passed to the component by the getServerSideProps() function on page load.

import { useState, useEffect } from 'react';

import { Layout, AddEdit } from 'components/users';
import { Spinner } from 'components';
import { userService, alertService } from 'services';

export default Edit;

function Edit({ id }) {
    const [user, setUser] = useState(null);

    useEffect(() => {
        // fetch user and set default form values if in edit mode
        userService.getById(id)
            .then(x => setUser(x))
            .catch(alertService.error)

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

    return (
        <Layout>
            <h1>Edit User</h1>
            {user ? <AddEdit user={user} /> : <Spinner /> }
        </Layout>
    );
}

export async function getServerSideProps({ params }) {
    return {
        props: { id: params.id }
    }
}
 

Add User Page

Path: /pages/users/add.jsx

The add user page is a thin wrapper around the add/edit user component without any user specified so the component is set to "add" mode.

import { Layout, AddEdit } from 'components/users';

export default Add;

function Add() {
    return (
        <Layout>
            <h1>Add User</h1>
            <AddEdit />
        </Layout>
    );
}
 

Users Index Page

Path: /pages/users/index.jsx

The users index page displays a list of all users in the Next.js tutorial app and contains buttons for adding, editing and deleting users. A useEffect hook is used to get all users from the user service and store them in local state by calling setUsers().

The delete button calls the deleteUser() function which first updates the user is local state with an isDeleting = true property so the UI displays a spinner on the delete button, it then calls userService.delete() to delete the user from the Next.js api, then removes the deleted user from local state to remove it from the UI.

import { useState, useEffect } from 'react';

import { Link, Spinner } from 'components';
import { Layout } from 'components/users';
import { userService } from 'services';

export default Index;

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

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

    function deleteUser(id) {
        setUsers(users.map(x => {
            if (x.id === id) { x.isDeleting = true; }
            return x;
        }));
        userService.delete(id).then(() => {
            setUsers(users => users.filter(x => x.id !== id));
        });
    }

    return (
        <Layout>
            <h1>Users</h1>
            <Link href="/users/add" className="btn btn-sm btn-success mb-2">Add User</Link>
            <table className="table table-striped">
                <thead>
                    <tr>
                        <th style={{ width: '30%' }}>First Name</th>
                        <th style={{ width: '30%' }}>Last Name</th>
                        <th style={{ width: '30%' }}>Username</th>
                        <th style={{ width: '10%' }}></th>
                    </tr>
                </thead>
                <tbody>
                    {users && users.map(user =>
                        <tr key={user.id}>
                            <td>{user.firstName}</td>
                            <td>{user.lastName}</td>
                            <td>{user.username}</td>
                            <td style={{ whiteSpace: 'nowrap' }}>
                                <Link href={`/users/edit/${user.id}`} className="btn btn-sm btn-primary mr-1">Edit</Link>
                                <button onClick={() => deleteUser(user.id)} className="btn btn-sm btn-danger btn-delete-user" disabled={user.isDeleting}>
                                    {user.isDeleting 
                                        ? <span className="spinner-border spinner-border-sm"></span>
                                        : <span>Delete</span>
                                    }
                                </button>
                            </td>
                        </tr>
                    )}
                    {!users &&
                        <tr>
                            <td colSpan="4">
                                <Spinner />
                            </td>
                        </tr>
                    }
                    {users && !users.length &&
                        <tr>
                            <td colSpan="4" className="text-center">
                                <div className="p-2">No Users To Display</div>
                            </td>
                        </tr>
                    }
                </tbody>
            </table>
        </Layout>
    );
}
 

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, global alert, 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 https://nextjs.org/docs/api-reference/next/head.

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 https://nextjs.org/docs/advanced-features/custom-app.

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, Alert } from 'components';

export default App;

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

    useEffect(() => {
        // on initial load - run auth check 
        authCheck(router.asPath);

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

        // on route change complete - run auth check 
        router.events.on('routeChangeComplete', authCheck)

        // unsubscribe from events in useEffect return function
        return () => {
            router.events.off('routeChangeStart', hideContent);
            router.events.off('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 
        setUser(userService.userValue);
        const publicPaths = ['/account/login', '/account/register'];
        const path = url.split('?')[0];
        if (!userService.userValue && !publicPaths.includes(path)) {
            setAuthorized(false);
            router.push({
                pathname: '/account/login',
                query: { returnUrl: router.asPath }
            });
        } else {
            setAuthorized(true);
        }
    }

    return (
        <>
            <Head>
                <title>Next.js 11 - User Registration and Login Example</title>
                
                {/* eslint-disable-next-line @next/next/no-css-tags */}
                <link href="//netdna.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet" />
            </Head>

            <div className={`app-container ${user ? 'bg-light' : ''}`}>
                <Nav />
                <Alert />
                {authorized &&
                    <Component {...pageProps} />
                }
            </div>
        </>
    );
}
 

Home Page

Path: /pages/index.jsx

The home page is a basic react function component that displays a welcome message to the logged in user and a link to the users section.

import { userService } from 'services';
import { Link } from 'components';

export default Home;

function Home() {
    return (
        <div className="p-4">
            <div className="container">
                <h1>Hi {userService.userValue?.firstName}!</h1>
                <p>You&apos;re logged in with Next.js & JWT!!</p>
                <p><Link href="/users">Manage Users</Link></p>
            </div>
        </div>
    );
}
 

Alert Service

Path: /services/alert.service.js

The alert service acts as the bridge between any component in the Next.js tutorial app and the alert component that displays notifications. It contains methods for sending, clearing and subscribing to alerts.

The AlertType object defines the types of alerts supported by the login tutorial app.

You can trigger alert notifications from any component in the application by calling one of the convenience methods for displaying different types of alerts: success(), error(), info() and warn().

Alert convenience method parameters

  • The first parameter is the alert message string, which can be plain text or HTML.
  • The second parameter is an optional options object that supports an autoClose boolean property and keepAfterRouteChange boolean property:
    • autoClose - if true tells the alert component to automatically close the alert after three seconds. Default is true.
    • keepAfterRouteChange - if true prevents the alert from being closed after one route change, this is handy for displaying messages after a redirect such as when a user is created or updated. Default is false.

For more info see React Hooks + Bootstrap - Alert Notifications.

import { Subject } from 'rxjs';
import { filter } from 'rxjs/operators';

export const alertService = {
    onAlert,
    success,
    error,
    info,
    warn,
    alert,
    clear
};

export const AlertType = {
    Success: 'Success',
    Error: 'Error',
    Info: 'Info',
    Warning: 'Warning'
};

const alertSubject = new Subject();
const defaultId = 'default-alert';

// enable subscribing to alerts observable
function onAlert(id = defaultId) {
    return alertSubject.asObservable().pipe(filter(x => x && x.id === id));
}

// convenience methods
function success(message, options) {
    alert({ ...options, type: AlertType.Success, message });
}

function error(message, options) {
    alert({ ...options, type: AlertType.Error, message });
}

function info(message, options) {
    alert({ ...options, type: AlertType.Info, message });
}

function warn(message, options) {
    alert({ ...options, type: AlertType.Warning, message });
}

// core alert method
function alert(alert) {
    alert.id = alert.id || defaultId;
    alert.autoClose = (alert.autoClose === undefined ? true : alert.autoClose);
    alertSubject.next(alert);
}

// clear alerts
function clear(id = defaultId) {
    alertSubject.next({ id });
}
 

User Service

Path: /services/user.service.js

The user service handles communication between the React front-end of the Next.js tutorial app and the backend API for everything related to users. It contains methods for logging in and out of the app, registering a new user, and standard CRUD methods for retrieving and updating user data. HTTP requests are sent with the help of the fetch wrapper.

On successful login the returned user is stored in browser local storage to keep the user logged in between page refreshes and browser sessions, if you prefer not to use local storage you can simply remove it from the user service and the application will continue to work correctly, except for staying logged in between page refreshes.

The user property exposes an RxJS Observable so any component can subscribe to be notified when a user logs in, logs out or updates their profile. The notification is triggered by the call to userSubject.next() from each of those methods. For more info on component communication with RxJS see the tutorial React + RxJS - Communicating Between Components with Observable & Subject.

The userValue getter allows other components to easily get the current value of the logged in user without having to subscribe to the user observable.

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 },
    login,
    logout,
    register,
    getAll,
    getById,
    update,
    delete: _delete
};

function login(username, password) {
    return fetchWrapper.post(`${baseUrl}/authenticate`, { username, password })
        .then(user => {
            // publish user to subscribers and store in local storage to stay logged in between page refreshes
            userSubject.next(user);
            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
    localStorage.removeItem('user');
    userSubject.next(null);
    Router.push('/account/login');
}

function register(user) {
    return fetchWrapper.post(`${baseUrl}/register`, user);
}

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

function getById(id) {
    return fetchWrapper.get(`${baseUrl}/${id}`);
}

function update(id, params) {
    return fetchWrapper.put(`${baseUrl}/${id}`, params)
        .then(x => {
            // update stored user if the logged in user updated their own record
            if (id === userSubject.value.id) {
                // update local storage
                const user = { ...userSubject.value, ...params };
                localStorage.setItem('user', JSON.stringify(user));

                // publish updated user to subscribers
                userSubject.next(user);
            }
            return x;
        });
}

// prefixed with underscored because delete is a reserved word in javascript
function _delete(id) {
    return fetchWrapper.delete(`${baseUrl}/${id}`);
}
 

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 tutorial app 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 tutorial 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 https://nextjs.org/docs/advanced-features/module-path-aliases.

{
    "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 tutorial 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 https://www.guidgenerator.com/).

module.exports = {
    reactStrictMode: true,
    serverRuntimeConfig: {
        secret: 'THIS IS USED TO SIGN AND VERIFY JWT TOKENS, REPLACE IT WITH YOUR OWN SECRET, IT CAN BE ANY STRING'
    },
    publicRuntimeConfig: {
        apiUrl: process.env.NODE_ENV === 'development'
            ? 'http://localhost:3000/api' // development api
            : 'http://localhost:3000/api' // production api
    }
}
 

Package.json

Path: /package.json

The package.json file contains project configuration information including scripts for running and building the Next.js tutorial 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 https://nextjs.org/docs/api-reference/cli.

{
    "name": "next-js-registration-login-example",
    "version": "0.1.0",
    "private": true,
    "scripts": {
        "dev": "next dev",
        "build": "next build",
        "start": "next start",
        "lint": "next lint"
    },
    "dependencies": {
        "@hookform/resolvers": "^2.7.1",
        "bcryptjs": "^2.4.3",
        "express-jwt": "^6.1.0",
        "jsonwebtoken": "^8.5.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!


Comments


Supported by