codersteps logo

Setting up our Next.js project for JWT Authentication

Abdessamad Ely
Abdessamad Ely
Software Engineer
Reading time: 5 min read  •  Published: in Next.js  •  Updated:

This tutorial series, assumes you already familiar with JWT (JSON Web Tokens) and the whole idea of how it works.

While I will try to explain each step of the way and every code we write, I won't be explaining what is JWT or how it works in theory.

In case you want to learn more about JSON Web Tokens here's a good Introduction to JSON Web Tokens.

Initializing a Next.js application

If you don’t already have a Next.js project, go ahead and create a new Next.js project using the app router.

To create a new Next.js project, navigate to your workspace then run npx create-next-app@latest nextjs_jwt_auth_system_with_api with all default options.

Once the create-next-app command is done, go ahead and open the project in your favourite code editor.

Installing jose and other dependencies

Now that we created our project, let's install the dependencies we will need for this tutorial.

Core dependencies

jose is a popular node dependency that we will use to sign and verify our JWT tokens. To install it let’s run npm i jose .

bcrypt is another popular node dependency that we will use to hash and compare user's password before storing them to our database.

To install it let’s run npm i bcrypt, and because we're using TypeScript we need to also install its types with npm i -D @types/bcrypt.

Good to have dependencies

We will use the date-fns dependency to calculate the expiration date for the remember me feature.

We will use the zod dependency to validate the user input schema in the registration and login forms.

We will use the server-only dependency to protect server-only code on build-time.

To install them let's run the commands: npm i date-fns, npm i server-only, and npm i zod.

Create Next.js Login page with Tailwind CSS

Under the app directory we will create a new dir (auth) to group all authentication related pages, then create the app/(auth)/login/page.tsx file:

app/(auth)/login/page.tsx
'use client'

import { FormEventHandler, useCallback, useState } from 'react'

export default function Login() {
  const [error, setError] = useState('')

  const onFormSubmit = useCallback<FormEventHandler<HTMLFormElement>>(
    async (e) => {
      e.preventDefault()
      const formData = new FormData(e.currentTarget)
      const res = await fetch('/api/login', {
        method: 'POST',
        body: formData,
      })

      if (!res.ok) {
        setError('Sorry! something went wrong.')
        return
      }

      const json = (await res.json()) as { success: boolean }
      if (!json.success) {
        setError('Invalid credentials.')
        return
      }

      location.href = '/dashboard'
    },
    []
  )

  return (
    <form className="pt-10" onSubmit={onFormSubmit}>
      <div className="w-96 mx-auto border border-gray-300 rounded-md space-y-3 px-6 py-8">
        <div className="space-y-5">
          <div className="pb-3">
            <h2 className="text-xl font-bold text-center">Login</h2>
          </div>
          <div className="space-y-1">
            <label htmlFor="username" className="text-sm font-bold select-none">
              Username
            </label>
            <input
              id="username"
              name="username"
              type="text"
              required
              tabIndex={1}
              placeholder="Username"
              className="block w-full text-sm p-3 bg-white border border-gray-300 placeholder:text-gray-400 focus:outline-none focus:border-gray-400 rounded"
            />
          </div>

          <div className="space-y-1">
            <label htmlFor="password" className="text-sm font-bold select-none">
              Password
            </label>
            <input
              id="password"
              name="password"
              type="password"
              required
              tabIndex={2}
              placeholder="Password"
              className="block w-full text-sm p-3 bg-white border border-gray-300 placeholder:text-gray-400 focus:outline-none focus:border-gray-400 rounded"
            />
          </div>

          <label className="inline-flex items-center space-x-1.5">
            <input type="checkbox" name="remember" tabIndex={3} />
            <span className="text-gray-500 text-xs leading-5 font-bold cursor-pointer select-none">
              Remember me
            </span>
          </label>

          <div className="flex justify-end">
            <button
              type="submit"
              tabIndex={4}
              className="bg-white h-10 border border-gray-400 text-gray-500 hover:border-gray-400 hover:text-black rounded text-sm font-medium px-3"
            >
              Log In
            </button>
          </div>

          {error && (
            <div className="font-medium text-xs text-red-500">{error}</div>
          )}
        </div>
      </div>
    </form>
  )
}

The login page, is straight forward, a simple React JSX with Tailwind CSS for styling and useState for tracking errors state.

We already implemented the onSubmit event, we read the form data, then post it to our server using the login API.

We also deal with the API response, in case of error, we stay on the page and show the error, otherwise we redirect to the /dashboard page.

We use the useCallback hook to avoid redefining the onFormSubmit handler on each render thus improving page performance.

If you visit the login route at localhost:3000/login you should see the login form.

Create Next.js Registration page with Tailwind CSS

Under the same (auth) folder, we will create a new route for the registration page, by creating the app/(auth)/register/page.tsx file:

app/(auth)/register/page.tsx
'use client'

import { FormEventHandler, useCallback, useState } from 'react'

export default function Register() {
  const [error, setError] = useState('')

  const onFormSubmit = useCallback<FormEventHandler<HTMLFormElement>>(
    async (e) => {
      e.preventDefault()
      const formData = new FormData(e.currentTarget)
      const res = await fetch('/api/register', {
        method: 'POST',
        body: formData,
      })

      if (!res.ok) {
        setError('Sorry! something went wrong.')
        return
      }

      const json = (await res.json()) as { success: boolean }
      if (!json.success) {
        setError('Invalid credentials.')
        return
      }

      location.href = '/login'
    },
    []
  )

  return (
    <form className="pt-10" onSubmit={onFormSubmit}>
      <div className="w-96 mx-auto border border-gray-300 rounded-md space-y-3 px-6 py-8">
        <div className="space-y-5">
          <div className="pb-3">
            <h2 className="text-xl font-bold text-center">Registration</h2>
          </div>
          <div className="space-y-1">
            <label htmlFor="username" className="text-sm font-bold select-none">
              Username
            </label>
            <input
              id="username"
              name="username"
              type="text"
              required
              tabIndex={1}
              placeholder="Username"
              className="block w-full text-sm p-3 bg-white border border-gray-300 placeholder:text-gray-400 focus:outline-none focus:border-gray-400 rounded"
            />
          </div>

          <div className="space-y-1">
            <label htmlFor="password" className="text-sm font-bold select-none">
              Password
            </label>
            <input
              id="password"
              name="password"
              type="password"
              required
              tabIndex={2}
              placeholder="Password"
              className="block w-full text-sm p-3 bg-white border border-gray-300 placeholder:text-gray-400 focus:outline-none focus:border-gray-400 rounded"
            />
          </div>

          <div className="flex justify-end">
            <button
              type="submit"
              tabIndex={3}
              className="bg-white h-10 border border-gray-400 text-gray-500 hover:border-gray-400 hover:text-black focus:outline-none focus:border-gray-400 rounded text-sm font-medium px-3"
            >
              Register
            </button>
          </div>
          {error && (
            <div className="font-medium text-xs text-red-500">{error}</div>
          )}
        </div>
      </div>
    </form>
  )
}

To make it simple, the registration form, is similar to the login form, but for your website you may need to extend it with more user information.

Also you can use your own form, the only important point is for you to understand how to use the login, and register API endpoints.

Once, we create a new account, we redirect the user to the login page so he can use he's new account to access the dashboard page.

Later, we will see how we use httpOnly cookie to login the user, so you can decide if you want to authenticate the user after registration or not.

If you visit the login route at localhost:3000/register you should see the registration form.

Chapter recap

In this chapter, we created a new Next.js project, then installed all dependencies we will use during this tutorial.

After that we created two new routes, the first for the login page, and the second for the registration page.

While we didn't create the API endpoints yet, we didn't test the authentication flow, but next we will work on the registration API endpoint.

Abdessamad Ely
Abdessamad Ely
Software Engineer

A web developer from Morocco Morocco flag , and the founder of coderstep.com a website that help developers grow and improve their skills with a set of step-by-step articles that just works.

Your Questions

  • No questions have been asked yet.

Submit your question

Please use the form bellow to submit your question, and it will be answered as soon as possible.