Backend Wiring

API

The backend is where the recovery flow becomes trustworthy: issue reset tokens, encrypt identifiers, talk to MongoDB safely, and only update credentials after all checks pass.

In this section

Follow the same recovery flow across installation, forms, auth, API wiring, and email delivery.

Structure

Keep the existing docs routes, but present them in a cleaner shell that matches the rest of the product UI.

Forgot-password endpoint

The request endpoint finds the user, creates a token, builds the reset URL, and sends the transactional email. This is the center of the recovery workflow because it coordinates storage, encryption, and outbound delivery.

Reject unknown emails without leaking extra implementation detail.
Generate a strong token and persist it to the user record before delivery.
Encrypt the email before placing it in a reset URL.

Request reset token

import db from '@/utils/db';
import User from '@/models/User';
import Env from '@/config/env';
import Cryptr from 'cryptr';
import cryptoRandomString from 'crypto-random-string';
import { sendEmail } from '@/config/mail';
import { render } from '@react-email/render';
import ForgotPasswordEmail from '@/app/emails/ForgotPasswordEmail';

export async function POST(req: Request) {
  const payload = await req.json();

  await db.connect();
  const user = await User.findOne({ email: payload.email });

  if (!user) {
    await db.disconnect();
    return Response.json({ message: 'Nonexistent user!' }, { status: 422 });
  }

  const randomStr = cryptoRandomString({
    length: 64,
    type: 'alphanumeric',
  });

  user.password_reset_token = randomStr;
  await user.save();
  await db.disconnect();

  const crypt = new Cryptr(Env.SECRET_KEY);
  const encryptedEmail = crypt.encrypt(user.email);
  const url =
    Env.APP_URL +
    '/reset-password/' +
    encryptedEmail +
    '?signature=' +
    randomStr +
    '&mail=' +
    encryptedEmail;

  const html = render(
    ForgotPasswordEmail({
      params: {
        name: user.name,
        email: user.email,
        url,
      },
    })
  );

  await sendEmail(payload.email, 'Reset Password', html);

  return Response.json({
    message: 'Email successfully sent. Please check your email.',
  });
}

Database connection utility

Keep MongoDB connection management in a dedicated utility so the API handlers stay focused on recovery logic. This also helps you reuse the same connection rules across auth, contact, and user flows.

MongoDB helper

import mongoose from 'mongoose';

const connection: { isConnected?: number } = {};

async function connect() {
  if (connection.isConnected) return;

  if (mongoose.connections.length > 0) {
    connection.isConnected = mongoose.connections[0].readyState;
    if (connection.isConnected === 1) return;
    await mongoose.disconnect();
  }

  const db = await mongoose.connect(process.env.MONGODB_URI!);
  connection.isConnected = db.connections[0].readyState;
}

async function disconnect() {
  if (connection.isConnected && process.env.NODE_ENV === 'production') {
    await mongoose.disconnect();
    connection.isConnected = 0;
  }
}

function convertDocToObj(doc: any) {
  doc._id = doc._id.toString();
  doc.createdAt = doc.createdAt.toString();
  doc.updatedAt = doc.updatedAt.toString();
  return doc;
}

const db = { connect, disconnect, convertDocToObj };

export default db;

Reset-password endpoint

When the user submits a new password, decrypt the email token, confirm the stored signature, hash the new password, and clear the reset token immediately so it cannot be reused.

Confirm reset and update password

import User from '@/models/User';
import Cryptr from 'cryptr';
import Env from '@/config/env';
import db from '@/utils/db';
import bcrypt from 'bcryptjs';

export async function POST(req: Request) {
  const payload = await req.json();
  const crypter = new Cryptr(Env.SECRET_KEY);
  const email = crypter.decrypt(payload.email);

  await db.connect();
  const user = await User.findOne({
    email,
    password_reset_token: payload.signature,
  });

  if (!user) {
    await db.disconnect();
    return Response.json(
      { message: 'Something went wrong. Please try again.' },
      { status: 400 }
    );
  }

  const salt = bcrypt.genSaltSync(10);
  user.password = bcrypt.hashSync(payload.password, salt);
  user.password_reset_token = '';
  await user.save();
  await db.disconnect();

  return Response.json({
    message: 'Password updated successfully. Please log in with your new password.',
  });
}
A good rule of thumb is simple: tokens should be single-use, password hashing should happen server-side, and every failure path should leave the account in a safe state.

Previous

Form

Next

SMTP