Implement user registration with Next.js + Neon + Prisma! A simple authentication system using a serverless database
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.
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.
About the Neon database
Neon is a service that provides Postgres in a serverless manner.
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.
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.
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:
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.
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.
Source: https://www.prisma.io/orm
Preparing the database
First, create an account with Neon.
Sign up from this page:
Specify the name of the database that can be created for free, the project name, and the region.
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.
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.
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
Click User in the list of model names.
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:
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.)
'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.
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.
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.
...
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.
You can check the registered data in Prisma Studio.
$ npx prisma studio
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.
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
Questions about this article 📝
If you have any questions or feedback about the content, please feel free to contact us.Go to inquiry form
Related Articles
Robust Authorization Design for GraphQL and REST APIs: Best Practices for RBAC, ABAC, and OAuth 2.0
2024/05/13Chat App (with Image/PDF Sending and Video Call Features)
2024/07/15Practical Component Design Guide with React × Tailwind CSS × Emotion: The Optimal Approach to Design Systems, State Management, and Reusability
2024/11/22Management Dashboard Features (Graph Display, Data Import)
2024/06/02Let's Build an Article Posting API with NestJS ─ Basics of Introducing Prisma and Implementing CRUD
2025/04/12Improving the Reliability of a NestJS App ─ Logging, Error Handling, and Testing Strategy
2024/09/11Deepening DB Design with NestJS × Prisma ─ Models, Relations, and Operational Design
2024/09/12NestJS × React × Railway: Implementing a Blog UI and Deploying to Production
2024/10/25









