Implement user registration with Next.js + Neon + Prisma! A simple authentication system using a serverless database

  • prisma
    prisma
  • nextjs
    nextjs
  • postgresql
    postgresql
Published on 2024/02/22

Introduction

Up to the previous article, we created the process that passes form input values to a server action.
This time, based on the received input values, we will implement the process that registers data into the database via a server action.
While introducing the database we will use and the library for accessing the database, we will explain the flow up to registration.
In the previous article, we left the server action always returning an error, but here we will fix it so that it returns a success message after registering to the database.

https://shinagawa-web.com/en/blogs/nextjs-server-actions-form-data

Goal for this article

We will actually perform user registration from the account registration screen. We will implement it up to the point where “Account has been registered” is displayed as shown below.
Also, since the process of logging in using the registered user will be covered in a later article, this time we will also introduce the steps to access the database directly to confirm that registration worked.

Image from Gyazo

About the Neon database

Neon is a service that provides Postgres in a serverless manner.

https://neon.tech/

Other companies that provide serverless databases include PlanetScale based on MySQL and Supabase based on Postgres, which are well known. Compared to those, Neon is relatively new, and we will use it for this implementation.

https://planetscale.com/

https://supabase.com/

As of September 2024, Neon provides one database per account for free. This makes it easy to start using it for testing. PlanetScale, which we introduced above, also used to provide one database for free, but recently switched to a paid model. In that sense, Neon may also switch from free to paid in the future.

About the ORM (Object-Relational Mapping) tool Prisma

We will use Prisma to connect from Next.js server actions to the database.

https://www.prisma.io/

Prisma itself provides various products, but here are the two we will use this time:

  • ORM: Can be used in backend services based on Node or TypeScript
  • Studio: Allows you to browse the database from your local environment via a browser

A list of supported databases and frameworks is summarized on this page:

https://www.prisma.io/stack

In addition to Neon, the databases we introduced earlier, PlanetScale and Supabase, are also supported. Therefore, although we will proceed with the implementation using Neon as the database this time, if you use Prisma, you can implement almost the same way with PlanetScale or Supabase.

Also, Prisma is tightly integrated with TypeScript, and by using the automatically generated type definitions based on the database schema, you can write type-safe queries. This allows you to detect typos and syntax errors in queries in advance, improving developer productivity.

Readable data model
Prisma’s schema is intuitive and allows you to declare database tables in a way that is readable for people who usually use JavaScript or TypeScript. You can define models manually, or automatically read the structure of an existing database (tables and columns) and generate a Prisma schema.

Image from Gyazo

Source: https://www.prisma.io/orm

When used together with the Prisma extension for VS Code, it also provides conveniences such as autocomplete that suggests APIs available for data retrieval, making implementation easier.

Image from Gyazo

Source: https://www.prisma.io/orm

Preparing the database

First, create an account with Neon.
Sign up from this page:

https://neon.tech/

Specify the name of the database that can be created for free, the project name, and the region.

Image from Gyazo

That’s all for creating the database.
Next, to check the information needed to access the created database, select Prisma, which we will use this time, and refer to .env.
You will see an entry starting with DATABASE_URL=; create an .env file at the project root and paste this value there.

Image from Gyazo

Preparing Prisma

Next, we will prepare to use Prisma.
The basic flow is also described in the official Prisma documentation, so we will proceed while referring to it.

https://www.prisma.io/docs/orm/prisma-client/setup-and-configuration/introduction

Installing the library to generate the schema definition

Run the following commands:

$ npm install prisma --save-dev
$ npx prisma

If you see usage examples for the command, you are good to go.

    ◭  Prisma is a modern DB toolkit to query, migrate and model your database (https://prisma.io)

    Usage

      $ prisma [command]

    Commands

                init   Set up Prisma for your app
            generate   Generate artifacts (e.g. Prisma Client)
                  db   Manage your database schema and lifecycle
             migrate   Migrate your database
              studio   Browse your data with Prisma Studio
            validate   Validate your Prisma schema
              format   Format your Prisma schema
             version   Displays Prisma version info
               debug   Displays Prisma debug info

Creating the schema definition

Set up Prisma with init, which was shown in the usage examples above.

$ npx prisma init
$ npx prisma init


✔ Your Prisma schema was created at prisma/schema.prisma
  You can now open it in your favorite editor.

warn You already have a .gitignore file. Don't forget to add `.env` in it to not commit any private information.

Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Set the provider of the datasource block in schema.prisma to match your database: postgresql, mysql, sqlite, sqlserver, mongodb or cockroachdb.
3. Run prisma db pull to turn your database schema into a Prisma schema.
4. Run prisma generate to generate the Prisma Client. You can then start querying your database.
5. Tip: Explore how you can extend the ORM with scalable connection pooling, global caching, and real-time database events. Read: https://pris.ly/cli/beyond-orm

More information in our documentation:
https://pris.ly/d/getting-started

Then, a schema.prisma file will be generated under the prisma folder, and you should see something like the following:

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

Let’s immediately write a model in this file.

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

+ model User {
+   id       String @id @default(cuid())
+   name     String
+   email    String @unique
+   password String
+ }

We create a model named User. The id is defined to be automatically generated, and email is defined to be unique.

Generating types from the schema definition

Generate types for use with TypeScript using the following command:

$ npx prisma generate
$ npx prisma generate
Environment variables loaded from .env
Prisma schema loaded from prisma/schema.prisma

✔ Generated Prisma Client (v5.18.0) to ./node_modules/@prisma/client in 73ms

Start by importing your Prisma Client (See: http://pris.ly/d/importing-client)

Tip: Need your database queries to be 1000x faster? Accelerate offers you that and more: https://pris.ly/tip-2-accelerate

We will write data retrieval logic using these types, but that will be after installing the Prisma Client, so for now this is fine.

Reflecting the schema definition in the database

By reflecting the schema definition in the database, a User table will be created.

$ npx prisma db push

Checking the database with Prisma Studio

Check that the table has been created in the database.

$ npx prisma studio

Image from Gyazo

Click User in the list of model names.

Image from Gyazo

You can confirm that id and name, which we set in the schema definition, are present.

Installing the Prisma Client to access the database from Next.js

Run the following command:

$ npm install @prisma/client

Required configuration when using Prisma Client in Next.js

When developing in a local environment, you may see a message like the following:

warn(prisma-client) There are already 10 instances of Prisma Client actively running.

When developing with npm run dev, Next.js automatically reloads and displays the latest state in the browser every time you change the code. At that timing, a new database connection is created each time, which causes the message above.

To handle local development, define the following file under the lib folder:

db.ts
import { PrismaClient } from '@prisma/client'

const prismaClientSingleton = () => {
  return new PrismaClient()
}

declare const globalThis: {
  prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;

const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()

export default prisma

if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma

We will use the prisma defined in this file to access the database.

Accessing the database from Next.js and registering users

The long setup is finally complete.
From here, we will write code in the server action and proceed with registering data into the database.

Hashing the password

In the authentication method we will use this time, passwords are stored in the database, and when logging in, we receive the password, check that it matches the stored password, and then grant access.
The implementation is simple, so it is recommended for building your first authentication mechanism, but password management is crucial.
This time, we will store the password after hashing it.
Later, when logging in, the user will enter the password on the login screen, and we will hash that password and check whether it matches the hashed password stored in the database.

Install a library that hashes arbitrary strings.

$ npm install bcryptjs
$ npm install -save-dev @types/bcryptjs

Modifying the server action

Rewrite the server action as follows. (Since there are many changes, it may be best to copy and paste as is.)

register.ts
'use server'

import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'
import bcryptjs from 'bcryptjs'
import type { z } from 'zod'
import db from '@/lib/db'
import { RegisterSchema } from '@/schema'

export const register = async (values: z.infer<typeof RegisterSchema>) => {
  const validatedFields = RegisterSchema.safeParse(values)

  if (!validatedFields.success) {
    return { error: 'Please correct the input.' }
  }

  const { name, email, password } = validatedFields.data
  const hashedPassword = await bcryptjs.hash(password, 10)

  try {
    await db.user.create({
      data: {
        name,
        email,
        password: hashedPassword,
      },
    })

    return { success: 'Account has been registered.' }
  } catch (e) {
    if (e instanceof PrismaClientKnownRequestError) {
      if (e.code === 'P2002') {
        return { error: 'This email address is already registered.' }
      }
    }
    console.log(e)
    return { error: 'An error has occurred.' }
  }
}

Here is an explanation of the code:

import bcryptjs from 'bcryptjs'

  const { name, email, password } = validatedFields.data
  const hashedPassword = await bcrypt.hash(password, 10)

After the schema check of the received data using Zod passes, we extract the necessary values.
Among them, for the password, we use bcryptjs to obtain the hashed password.

import db from '@/lib/db'


  try {
    await db.user.create({
      data: {
        name,
        email,
        password: hashedPassword,
      },
    })

    return { success: 'Account has been registered.' }
  }

This is the actual process that registers data into the database. It registers the name, email address, and hashed password. When writing the code in VS Code, typing db. will show suggestions for available APIs and models, which is very convenient.

Image from Gyazo

If no error occurs during database registration, it returns the message “Account has been registered.” to the input screen.

import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library'

catch (e) {
    if (e instanceof PrismaClientKnownRequestError) {
      if (e.code === 'P2002') {
        return { error: 'This email address is already registered.' }
      }
    }
    console.log(e)
    return { error: 'An error has occurred.' }
  }

This is the process when an error occurs during database registration. As we defined in the model earlier, the email address must be unique, so if an attempt is made to register with an email address that is already in use, we return the error “This email address is already registered.”

There are many types of database-related errors, and there may be others that should be reported to the user. The document below summarizes error codes and their meanings, so you can find the necessary errors there and add them to your conditional branches.

https://www.prisma.io/docs/orm/reference/error-reference#error-codes

In this article, we return only “An error has occurred.” for all errors other than the one above.

Modifying the input form

Since we have modified the server action so that it can return a success message, we will modify the account registration screen's input form so that it can display the success message.

register-form.tsx

...

  const onSubmit = async (values: z.infer<typeof RegisterSchema>) => {
    setError('')
    setSuccess('')
    setTransition(async () => {
      try {
        const response = await register(values)
        if (response.error) {
          setError(response.error)
        } else {
-           // setSuccess(response.success)
+           setSuccess(response.success)
        }
      } catch (e) {
        setError('An error has occurred.')
      }
    })
  }

Checking the behavior

We will actually perform user registration from the account registration screen.
If “Account has been registered.” is displayed as shown below, it is working correctly.

Image from Gyazo

You can check the registered data in Prisma Studio.

$ npx prisma studio

Image from Gyazo

You can confirm that the name and email address are registered as entered, and that the password contains a different string from the input (i.e., it is hashed).

Next, try clicking “Create account” again using the email address you just used.

Image from Gyazo

If the message “This email address is already registered.” is displayed, the behavior is as expected.

Conclusion

In this article, we set up the database and ORM tool, and implemented the registration process to the database from a Next.js server action. We also used a convenient tool called Prisma Studio to view the contents of the database.
In the next and subsequent articles, we will implement the ability to retrieve the registered information and enable logging in.

Next article

https://shinagawa-web.com/en/blogs/nextjs-middleware-auth-access-control

Xでシェア
Facebookでシェア
LinkedInでシェア

Questions about this article 📝

If you have any questions or feedback about the content, please feel free to contact us.
Go to inquiry form