Kazi Rahamatullah
Kazi Rahamatullah
AboutProjectsBlogContact
UsesBooks
ResumeView CV
Kazi Rahamatullah

© Copyright 2026 Kazi Rahamatullah

AboutProjectsBlogBooksUses
Twitter/XGitHubProduct HuntCodeSandbox
Back to Blog
Next.jsNext-AuthMongoDBMongooseRegistrationApp Router

Next.js Authentication || Register User To MongoDB With Next-Auth V5

Register users to MongoDB with Mongoose in Next.js App Router — registration UI, /api/register route, bcrypt hashing, user schema, createUser queries, and Next-Auth v5 Credentials login integration.

Jul 6, 202615 min read

Introduction

This guide covers registering users to MongoDB and wiring them into Next-Auth v5 credential login. You own the full loop: registration form → API route → hashed password in MongoDB → authorize lookup on sign-in.

Next-Auth does not provide registration — you build /api/register, store users with Mongoose, then point the Credentials provider at the same collection.

plaintext
      Next.js Auth v5 with MongoDB
                  │
    ┌─────┬───────┼───────┬─────────┬──────────┐
    │     │       │       │         │          │
   Reg   API   MongoDB  Data    Security   Login
   UI   Route   Setup  Model   Encryption  (auth.ts)

What you will build:

BranchWhat it covers
1. Registration UIRegister link, /register page, form with name/email/password, redirect to login
2. API route handlerPOST /api/register, JSON parsing, try/catch, status codes
3. MongoDB setupMONGODB_URI, Mongoose install, connection utility
4. Data modelingUser schema, required fields, singleton model export
5. Security & encryptionbcryptjs, bcrypt.hash, salt rounds
6. Database queriesqueries/users.ts, createUser, user.create()
7. Login flow integrationauth.ts — findOne, bcrypt.compare, authorize

Note

This guide assumes you already understand Next-Auth v5 Credentials login. For the client-side login form and redirect: false pattern, see Next.js Authentication With Next-Auth V5 || Credential Provider.

Quick index

#Section
1Install & project structure
2MongoDB setup
3Data modeling — user schema
4Security & encryption
5Database queries
6API route handler
7Registration UI
8Login flow integration — auth.ts
9Production notes & troubleshooting

1. Install & project structure

bash
npm install next-auth@5 mongoose bcryptjs
npm install -D @types/bcryptjs
npx auth secret

Environment variables

bash
# .env.local
AUTH_SECRET=your_generated_secret_here
MONGODB_URI=mongodb+srv://<user>:<password>@cluster.mongodb.net/myapp
VariablePurpose
AUTH_SECRETEncrypts Next-Auth session JWT
MONGODB_URIMongoDB Atlas or local connection string

Folder structure

plaintext
├── app/
│   ├── api/
│   │   ├── auth/[...nextauth]/route.ts
│   │   └── register/route.ts              # POST — create user
│   ├── login/page.tsx                     # Link to /register
│   ├── register/page.tsx                  # Registration page
│   └── dashboard/page.tsx
├── components/
│   ├── RegistrationForm.tsx               # Client form
│   ├── CredentialLoginForm.tsx            # From credentials guide
│   └── LogoutButton.tsx
├── lib/
│   ├── mongodb.ts                         # Connection utility
│   └── queries/users.ts                   # createUser, getUserByEmail
├── models/
│   └── user-model.ts                      # Mongoose schema
└── auth.ts                                # Credentials + MongoDB authorize

Back to index


2. MongoDB setup

Connection string in .env

Create a free cluster at MongoDB Atlas, add a database user, whitelist your IP (or 0.0.0.0/0 for development), and copy the connection string into MONGODB_URI.

Mongoose ODM installation

Mongoose is already installed above (mongoose package). It provides schemas, validation, and the create / findOne methods used throughout this guide.

DB connection utility

Next.js hot reload creates new connections on every file save unless you cache the connection globally.

ts
// lib/mongodb.ts
import mongoose from 'mongoose'
 
const MONGODB_URI = process.env.MONGODB_URI
 
if (!MONGODB_URI) {
  throw new Error('MONGODB_URI is not defined in environment variables')
}
 
interface MongooseCache {
  conn: typeof mongoose | null
  promise: Promise<typeof mongoose> | null
}
 
declare global {
  // eslint-disable-next-line no-var
  var mongooseCache: MongooseCache | undefined
}
 
const cached: MongooseCache = global.mongooseCache ?? { conn: null, promise: null }
global.mongooseCache = cached
 
export async function connectDB() {
  if (cached.conn) return cached.conn
 
  if (!cached.promise) {
    cached.promise = mongoose.connect(MONGODB_URI)
  }
 
  cached.conn = await cached.promise
  return cached.conn
}

Note

Call connectDB() before any Mongoose query in API routes and authorize. The cached connection prevents MongoServerSelectionError: too many connections during next dev hot reloads.

User collection creation

Mongoose creates the users collection automatically on the first User.create() call. No manual collection setup required — the schema in the next section defines the document shape.

Tip

Name your model User — Mongoose pluralizes it to the users collection by default. Consistent naming avoids confusion when browsing Atlas.

Back to index


3. Data modeling — user schema

Define the user document shape in a dedicated model file. All three fields are required — registration and login depend on them.

ts
// models/user-model.ts
import mongoose from 'mongoose'
 
const userSchema = new mongoose.Schema(
  {
    name: {
      type: String,
      required: [true, 'Name is required'],
      trim: true,
    },
    email: {
      type: String,
      required: [true, 'Email is required'],
      unique: true,
      lowercase: true,
      trim: true,
    },
    password: {
      type: String,
      required: [true, 'Password is required'],
    },
  },
  { timestamps: true },
)
 
// Singleton model export — prevents OverwriteModelError on hot reload
export const User = mongoose.models.User ?? mongoose.model('User', userSchema)
FieldTypeRules
nameStringRequired, trimmed
emailStringRequired, unique, lowercase
passwordStringRequired — stores bcrypt hash

Warning

The password field stores the bcrypt hash, never plain text. Hashing happens in the API route before createUser — the schema only enforces presence.

Note

mongoose.models.User ?? mongoose.model('User', userSchema) is the standard Next.js pattern. Without it, saving a file during next dev re-registers the model and throws OverwriteModelError.

Back to index


4. Security & encryption

Passwords must be hashed before they touch MongoDB. Use bcryptjs with a salt rounds value of 10 — the industry default for web apps.

ts
import bcrypt from 'bcryptjs'
 
const SALT_ROUNDS = 10
 
const hashedPassword = await bcrypt.hash(plainPassword, SALT_ROUNDS)
// → store hashedPassword in MongoDB, discard plainPassword
TopicDetail
bcryptjs libraryPure JS bcrypt — works in Next.js Route Handlers without native bindings
bcrypt.hashOne-way transform; cannot recover original password
Salt rounds (10)~100ms per hash — slows brute-force without hurting UX on register/login

Performance

Salt rounds 10 is the sweet spot for most apps. Rounds 12+ doubles hash time per login — only increase if your threat model requires it and you can accept slower auth.

Tip

Never hash passwords on the client. The browser sends plain text over HTTPS; the server hashes before User.create(). Client-side hashing does not replace server-side hashing.

Back to index


5. Database queries

Keep database operations in a dedicated queries file — API routes and auth.ts import helpers instead of writing Mongoose calls inline.

ts
// lib/queries/users.ts
import { connectDB } from '@/lib/mongodb'
import { User } from '@/models/user-model'
 
type CreateUserInput = {
  name: string
  email: string
  password: string // already hashed
}
 
export async function createUser({ name, email, password }: CreateUserInput) {
  await connectDB()
  return User.create({ name, email, password })
}
 
export async function getUserByEmail(email: string) {
  await connectDB()
  return User.findOne({ email: email.toLowerCase() }).lean()
}
FunctionMongoose methodUsed by
createUserUser.create()POST /api/register
getUserByEmailUser.findOne()auth.ts authorize

Note

createUser expects an already-hashed password. Hashing belongs in the API route so the queries layer stays a thin data access layer.

Back to index


6. API route handler

The register endpoint receives JSON from the registration form, validates input, hashes the password, and persists the user.

ts
// app/api/register/route.ts
import { NextResponse } from 'next/server'
import bcrypt from 'bcryptjs'
import { createUser } from '@/lib/queries/users'
 
const SALT_ROUNDS = 10
 
export async function POST(request: Request) {
  try {
    const body = await request.json()
    const { name, email, password } = body
 
    // Validate payload
    if (!name?.trim() || !email?.trim() || !password) {
      return NextResponse.json({ error: 'Name, email, and password are required.' }, { status: 400 })
    }
 
    if (password.length < 8) {
      return NextResponse.json({ error: 'Password must be at least 8 characters.' }, { status: 400 })
    }
 
    const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS)
 
    await createUser({
      name: name.trim(),
      email: email.trim().toLowerCase(),
      password: hashedPassword,
    })
 
    return NextResponse.json({ message: 'Account created successfully.' }, { status: 201 })
  } catch (error) {
    // Duplicate email — MongoDB unique index violation
    if (error instanceof Error && 'code' in error && (error as { code: number }).code === 11000) {
      return NextResponse.json({ error: 'An account with this email already exists.' }, { status: 409 })
    }
 
    console.error('[POST /api/register]', error)
    return NextResponse.json({ error: 'Something went wrong. Please try again.' }, { status: 500 })
  }
}

NextResponse status codes

StatusWhen
201User created successfully
400Missing or invalid fields
409Email already registered (duplicate key)
500Unexpected server error

Warning

Wrap the handler in try/catch. Mongoose connection failures, duplicate keys, and validation errors must return JSON — not an unhandled 500 HTML page — so the registration form can show inline feedback.

Back to index


7. Registration UI

Registration link on login page

tsx
// app/login/page.tsx
import Link from 'next/link'
import { auth } from '@/auth'
import { redirect } from 'next/navigation'
import { CredentialLoginForm } from '@/components/CredentialLoginForm'
 
export default async function LoginPage() {
  const session = await auth()
  if (session) redirect('/dashboard')
 
  return (
    <main className="mx-auto max-w-md px-6 py-20">
      <h1 className="text-2xl font-bold">Sign in</h1>
      <div className="mt-8">
        <CredentialLoginForm />
      </div>
      <p className="mt-6 text-center text-sm text-neutral-600">
        Don&apos;t have an account?{' '}
        <Link href="/register" className="font-semibold underline">
          Create one
        </Link>
      </p>
    </main>
  )
}

Register route

tsx
// app/register/page.tsx
import Link from 'next/link'
import { auth } from '@/auth'
import { redirect } from 'next/navigation'
import { RegistrationForm } from '@/components/RegistrationForm'
 
export default async function RegisterPage() {
  const session = await auth()
  if (session) redirect('/dashboard')
 
  return (
    <main className="mx-auto max-w-md px-6 py-20">
      <h1 className="text-2xl font-bold">Create account</h1>
      <p className="mt-2 text-neutral-600">Register with your name, email, and password.</p>
      <div className="mt-8">
        <RegistrationForm />
      </div>
      <p className="mt-6 text-center text-sm text-neutral-600">
        Already have an account?{' '}
        <Link href="/login" className="font-semibold underline">
          Sign in
        </Link>
      </p>
    </main>
  )
}

Registration form component

Client component with handleSubmit, fetch to the API, error state, and redirect to login on success.

tsx
// components/RegistrationForm.tsx
'use client'
 
import { useState } from 'react'
import { useRouter } from 'next/navigation'
 
export function RegistrationForm() {
  const router = useRouter()
  const [error, setError] = useState<string | null>(null)
  const [pending, setPending] = useState(false)
 
  async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setError(null)
    setPending(true)
 
    const formData = new FormData(event.currentTarget)
    const name = formData.get('name') as string
    const email = formData.get('email') as string
    const password = formData.get('password') as string
 
    try {
      const response = await fetch('/api/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name, email, password }),
      })
 
      const data = await response.json()
 
      if (!response.ok) {
        setError(data.error ?? 'Registration failed.')
        setPending(false)
        return
      }
 
      // Router redirect — send user to login after successful registration
      router.push('/login?registered=true')
    } catch {
      setError('Network error. Please try again.')
      setPending(false)
    }
  }
 
  return (
    <form onSubmit={handleSubmit} className="flex flex-col gap-4">
      <div>
        <label htmlFor="name" className="mb-1 block text-sm font-semibold">
          Name
        </label>
        <input
          id="name"
          name="name"
          type="text"
          autoComplete="name"
          required
          className="w-full rounded-lg border px-4 py-3"
          placeholder="Jane Doe"
        />
      </div>
 
      <div>
        <label htmlFor="email" className="mb-1 block text-sm font-semibold">
          Email
        </label>
        <input
          id="email"
          name="email"
          type="email"
          autoComplete="email"
          required
          className="w-full rounded-lg border px-4 py-3"
          placeholder="jane@example.com"
        />
      </div>
 
      <div>
        <label htmlFor="password" className="mb-1 block text-sm font-semibold">
          Password
        </label>
        <input
          id="password"
          name="password"
          type="password"
          autoComplete="new-password"
          required
          minLength={8}
          className="w-full rounded-lg border px-4 py-3"
          placeholder="••••••••"
        />
      </div>
 
      {error && (
        <p
          role="alert"
          className="rounded-lg border border-red-400 bg-red-50 px-4 py-3 text-sm font-medium text-red-700">
          {error}
        </p>
      )}
 
      <button
        type="submit"
        disabled={pending}
        className="w-full rounded-lg border px-4 py-3 font-semibold disabled:opacity-60">
        {pending ? 'Creating account…' : 'Create account'}
      </button>
    </form>
  )
}
Form featureImplementation
Input fieldsname, email, password with labels and autoComplete
Submit buttonTriggers handleSubmit → POST /api/register
handleSubmitpreventDefault, fetch JSON payload, error handling
Router redirectrouter.push('/login?registered=true') on 201

Tip

Show a success banner on the login page when ?registered=true is present — confirms registration worked before the user signs in with their new credentials.

Back to index


8. Login flow integration — auth.ts

After registration, users sign in through the same Credentials provider — but authorize now queries MongoDB instead of a mock array.

ts
// auth.ts
import NextAuth from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
import bcrypt from 'bcryptjs'
import { getUserByEmail } from '@/lib/queries/users'
 
export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    Credentials({
      name: 'credentials',
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) {
          return null
        }
 
        const email = String(credentials.email)
        const password = String(credentials.password)
 
        // User lookup — MongoDB findOne by email
        const user = await getUserByEmail(email)
        if (!user) return null
 
        // Password verification — bcrypt.compare plain vs stored hash
        const passwordsMatch = await bcrypt.compare(password, user.password)
        if (!passwordsMatch) return null
 
        return {
          id: user._id.toString(),
          name: user.name,
          email: user.email,
        }
      },
    }),
  ],
  pages: {
    signIn: '/login',
  },
  session: {
    strategy: 'jwt',
  },
  callbacks: {
    async jwt({ token, user }) {
      if (user) token.id = user.id
      return token
    },
    async session({ session, token }) {
      if (session.user && token.sub) {
        session.user.id = token.sub
      }
      return session
    },
  },
})

Authorize method logic

plaintext
signIn('credentials', { email, password })
         ↓
authorize(credentials)
         ↓
getUserByEmail(email)     →  User.findOne() in MongoDB
         ↓
bcrypt.compare(password, user.password)
         ↓
return { id, name, email }  →  JWT session created
StepMethodOn failure
User lookupgetUserByEmail → User.findOne()return null
Password verificationbcrypt.compare()return null
SuccessReturn user object for JWT—

Note

Registration writes the hash; login reads and compares it. Both flows share lib/queries/users.ts — single source of truth for how users are stored and retrieved.

API route for Next-Auth

ts
// app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth'
 
export const { GET, POST } = handlers

Back to index


9. Production notes & troubleshooting

End-to-end flow

plaintext
/register  →  POST /api/register  →  bcrypt.hash  →  User.create()  →  redirect /login
/login     →  signIn('credentials')  →  authorize  →  findOne  →  bcrypt.compare  →  session
/dashboard →  auth()  →  protected

Before going live

StepAction
1Set MONGODB_URI and AUTH_SECRET in production env vars
2Restrict MongoDB Atlas IP allowlist to your host
3Ensure unique index on email (Mongoose unique: true handles this)
4Test register → login → dashboard flow
5Verify duplicate email returns 409, not 500

Common errors

ErrorCauseFix
MONGODB_URI is not definedMissing env varAdd to .env.local and Vercel settings
OverwriteModelErrorModel re-registered on hot reloadUse singleton export pattern
E11000 duplicate keyEmail already existsReturn 409 in catch block
Login fails after registerPlain password saved instead of hashHash in API route before createUser
Too many connectionsNo connection cacheUse lib/mongodb.ts global cache

Warning

Never expose MONGODB_URI with NEXT_PUBLIC_ prefix. The connection string contains credentials — server-only environment variables only.

Interview Answer

"How do you register users to MongoDB with Next-Auth v5?"

Build a POST /api/register route that parses JSON, validates fields, hashes the password with bcrypt.hash(password, 10), and calls User.create() via a createUser helper. Define a Mongoose user schema with required name, email, and password fields. Cache the MongoDB connection in lib/mongodb.ts. On the client, a registration form posts to the API and redirects to /login on 201. Update auth.ts Credentials authorize to findOne by email and bcrypt.compare the password against the stored hash.

Back to index


Summary

Mind map branchKey files
Registration UIRegistrationForm.tsx, /register/page.tsx, login page link
API route handlerapp/api/register/route.ts — POST, JSON, try/catch, 201/400/409/500
MongoDB setupMONGODB_URI, lib/mongodb.ts, Mongoose install
Data modelingmodels/user-model.ts — name, email, password, singleton export
Security & encryptionbcryptjs, bcrypt.hash, salt rounds 10
Database querieslib/queries/users.ts — createUser, getUserByEmail
Login flow integrationauth.ts — authorize, findOne, bcrypt.compare

That is the complete register user to MongoDB + Next-Auth v5 pipeline — from sign-up form to credential login against a real database.

Share this article

XLinkedInFacebook
Kazi Rahamatullah

Written by

Kazi Rahamatullah

FullStack Developer

X / TwitterGitHubLinkedIn

Subscribe to my newsletter

Stay up to date and get notified when I share new contents.

No spam ever, unsubscribe anytime