Platform
Docs
Solutions
ContactLog In

Create Your Free Developer Account

Courier makes it easy to send SMS, email, push and in-app notifications with a single API call. We have a REST API, client SDKs and Docs that you'll love đź’ś

Sign-up

Next.js + Courier
TUTORIAL

How to Send Password Resets via SMS and email using Node.js and Next.js

Carter Rabasa

May 30, 2023

When you’re building a web application, there’s an immediate decision to make about how to handle Users and Authentication. There are lots of services (Auth0, Clerk) and libraries (Passport.js) to choose from, and the right choice will depend on the requirements of the application that you’re building. Regardless of the decision here, it’s important that end users have a simple way to reset their passwords and to receive those token notifications on their preferred channel (sms, email, etc).

In this tutorial, I’m going to build a simple and secure password reset flow using Courier and the latest Next.js 13 (app router) that allows the end user to receive a token using either SMS or email. We’re going to cover:

  • Creating a new Next.js web application
  • Configuring Courier to handle SMS and email notifications
  • Using Courier to store user profile and preference data
  • Using Vercel KV for token storage
  • Routing token notifications based on user preferences for email or SMS

There are a few prerequisites for completing this tutorial:

You can find the full source code for this application on Github and a live demo of this app hosted on Vercel.

Creating a Next.js web application

In order to build a Next.js app, you’ll need to have Node.js installed. My preference these days is to use NVM (Node Version Manager) to install Node.js. It makes it easy to install multiple versions of Node.js and switch between them in your projects.

Once you’ve installed Node.js, open up a terminal and run the following command to install Next.js:

1
npx create-next-app@latest

You’ll be prompted to answer several questions, but it’s fine to stick to the defaults. Once this process is complete, a new directory will be created and loaded with all of the default files for this app.

Change into this new directory and create a .env.local file to store secrets for Courier and Vercel. We’ll populate this file while we’re building and testing on localhost, and you’ll just need to remember to migrate these environment variables to whatever platform or infra you deploy your app to.

Get Courier API Credentials

Log-in to your Courier account and click on the gear icon and then API Keys. When you create a Courier account, we automatically create two Workspaces for you, one for testing and one for production. Each workspace has its own set of data and API keys.

For simplicity, we’re going to stick to the “production” workspace. Copy the "published" production API Key and paste into into .env.local using the following key:

1
COURIER_AUTH_TOKEN=pk_XXX

The "published" API key means that when you send a notification and reference a template, it will only use the published version of that template. If you’re editing a template and it is auto-saved as a draft (but not published) you can use the "draft" API key to use that draft template when sending. Once again, for the sake of simplicity, we're going to stick to published templates and the published API key.

Configure Your Email and SMS Providers

Click on "Channels" in the left nav. This is where you can configure the providers you'd like to use to deliver notifications. These providers are grouped into channels, like SMS and email. For the purposes of this tutorial, you need to configure an SMS provider (like Twilio) and an email provider (like Postmark).

Create a Notification Template

Click on "Designer" on the left nav. You'll see a default notification template called "Welcome to Courier". Notification templates make it easy for developers to customize what a single notification (i.e. a password reset notification) looks like across different channels like email, SMS, push, etc.

Create a new template and call it "Password Reset Token". Leave the Subscription Topic set to "General Notifications". The next screen will allow you to select the channels you’d like to design a template for. Please select "email" and "SMS".

On the left, you’ll see a list of the channels you selected. Make sure to configure each channel to use the provider you configured above. If you only have one provider for each channel, Courier will default to it and there’s nothing for you to do. If you had multiple providers for a channel, you’d see a warning and be asked to select one.

In your browser’s URL bar, you should see a URL that looks like this:

https://app.courier.com/designer/notifications/XXX/design?channel=YYY

The XXX is the unique ID of this template. Copy that ID and paste it into your .env.local:

1
COURIER_TEMPLATE=XXX

Get Vercel KV Credentials

Log-in to your Vercel account and click on "Storage". Click "Create Database" and select KV (Durable Redis). Give the database any name you like and stick to the default configuration options for now.

Once your new KV database is created, you’ll see a "Quickstart" section just below the name of your database. In that section click the ".env.local" tab. This displays the environment variables you need to interact with the database from your app. Click "copy snippet" and paste those values into your app’s .env.local file:

1
KV_URL="redis://default:xxx@1234.kv.vercel-storage.com:35749"
2
KV_REST_API_URL="https://1234.kv.vercel-storage.com"
3
KV_REST_API_TOKEN="yyy"
4
KV_REST_API_READ_ONLY_TOKEN="zzz"

Let’s Start Coding!

Ok, now that we have our services and configuration out of the way, let's dive into the code. This app is built on the latest Next.js 13 with the app router. These application will support the following flow:

  1. Create a dummy user to test the password reset
  2. Forgot Password page - user can enter an email address or phone number
  3. Enter Token page - user can enter the token that was sent to them
  4. Change Password page - user can enter a new password

New User Page

The new Next.js has strict conventions on how to create routes for client-side and server-side code. To create the new user page, first create a new directory under app called new-user and then create a file in that directory called page.js. Paste the following code into the file:

1
export default function NewUser(request) {
2
return (
3
<main className="flex min-h-screen flex-col items-center justify-between p-24">
4
<p>Hello New User Page</p>
5
</main>
6
)
7
}

Spin-up your local dev server to double-check that everything is working properly. In the root of your project run:

1
npm run dev

Then open up http://locahost:3000/new-user in your browser and confirm that you see "Hello New User Page". Once you’ve verified the app is working, we can move on to building out this page.

At the top of the file, including the following:

1
'use client'
2
import { useRouter } from 'next/navigation'
3
import { useState } from 'react'

The use client directive tells Next.js that this component should only run on the client-side. Take a minute to familiarize yourself with React.js client and server components and how they fit into the design of the new Next.js. The useRouter and useState imports give us tools to handle redirection and displaying error messages.

Now, replace the <p>Hello New User Page</p> with the following code:

1
<form onSubmit={onCreateUser} className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
2
{ error && ( <div className="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative">{ error }</div>) }
3
<div className="mb-4">
4
<p>Create a FAKE user so that we can test the password reset flow.</p>
5
<p>Please enter your REAL email address and phone number in order to see how the demo works.</p>
6
<p>NOTE: all your data will be purged after 5 minutes.</p>
7
</div>
8
<div className="mb-4">
9
<label htmlFor="name" className="block text-gray-700 text-sm font-bold mb-2">Full Name</label>
10
<input type="text" name="name" id="name" className="mb-2 shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"></input>
11
<label htmlFor="email" className="block text-gray-700 text-sm font-bold mb-2">Email Address</label>
12
<input type="email" name="email" id="email" className="mb-2 shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"></input>
13
<label htmlFor="phone" className="block text-gray-700 text-sm font-bold mb-2">Mobile Number</label>
14
<input type="text" name="phone" id="phone" className="mb-2 shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"></input>
15
<label htmlFor="password" className="block text-gray-700 text-sm font-bold mb-2">Password</label>
16
<input type="password" name="password" id="password" className="mb-2 shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"></input>
17
<label htmlFor="preference" className="block text-gray-700 text-sm font-bold mb-2">Notification Preference</label>
18
<select name="preference" id="preference">
19
<option value="email">Email</option>
20
<option value="phone">SMS</option>
21
</select>
22
</div>
23
<input type="submit" value="Create User" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"></input>
24
</form>

Tailwind classes aside, this is a pretty basic HTML form that lets you create a quick and dirty user for the sake of testing the password reset flow. In a real world application, you’ll need to have a proper user management set-up.

The form submission itself triggers a client-side JS function that we will now define. Just below the export default function NewUser(request) { line, add the following code:

1
const router = useRouter()
2
const [ error, setError ] = useState()
3
4
async function onCreateUser(event) {
5
event.preventDefault()
6
const formData = new FormData(event.target)
7
const payload = {
8
name: formData.get('name'),
9
email: formData.get('email'),
10
phone: formData.get('phone'),
11
password: formData.get('password'),
12
preference: formData.get('preference'),
13
}
14
const response = await createUser(payload)
15
if (response.error) {
16
setError(response.error)
17
}
18
else if (response.redirect) {
19
router.push(`${response.redirect}?message=${response.message}`)
20
}
21
return true
22
}

The router allows us to trigger client-side routing when it's time to move on to the next page. The error and setError hooks allow us to display error messages to the user.

The function onCreateUser does the work of parsing the HTML form, building a JSON payload, calling the createUser and then either redirecting (success) or displaying an error (failure).

Finally, below the imports at the top of the file, include the following function:

1
// submit this data to create-user/route.js
2
async function createUser(payload) {
3
const res = await fetch('/create-user', { method: 'POST', body: JSON.stringify(payload) })
4
if (!res.ok) return undefined
5
return res.json()
6
}

This function uses JS native fetch to call our backend to create the user. Go ahead and reload http://localhost:3000/new-user and ensure that the form renders properly. But don’t submit it! We haven’t written the backend, so let’s do that now.

Create User Route

Create a new directory under app called create-user and create a file called route.js. Route handlers are used for backend code and can support GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS HTTP methods. In our case we’re going to implement a POST handler with the following code:

1
import { NextResponse } from 'next/server'
2
import { kv } from '@vercel/kv'
3
import { CourierClient } from '@trycourier/courier'
4
import { createUser } from '../../models/users'
5
import { setSession } from '../../session'
6
7
const courier = CourierClient({ authorizationToken: process.env.courier_auth_token })
8
9
export async function POST(request) {
10
const data = await request.json()
11
// get full name, phone number, email and password from form payload
12
const { name, email, phone, password, preference } = data
13
// create the User
14
const user_id = await createUser({ name, email, phone, password, preference })
15
// create the Courier Profile for this User
16
await courier.mergeProfile({
17
recipientId: user_id,
18
profile: {
19
phone_number: phone,
20
email,
21
name,
22
// Courier supports storing custom JSON data for Profiles
23
custom: {
24
preference
25
}
26
}
27
})
28
// return response
29
const response = NextResponse.json({
30
redirect: '/',
31
message: 'Your User has been created đź‘Ť'
32
})
33
setSession(response, 'user_id', user_id)
34
return response
35
}

This function takes the data passed-in and uses it to create a new User. Once you’ve created the User and have a unique user_id you can create a profile in Courier to store this information. Storing a subset of a user's profile information in Courier makes it easy to customize notifications and respect a user's routing preferences.

After the User has been created, a response is sent to the client with information about where to redirect the user to and what message to display. In this case, we're simply going back to the index page.

Before we can execute this route, we need to implement a simple user model and session service. Remember, this code is just for demonstration purposes, so make sure you’re handling Users and Sessions properly when you build your app.

The User Model

Create a directory at the root of your project called models and create a new file called users.js and paste in the following code:

1
import { kv } from '@vercel/kv'
2
import { createHash } from 'node:crypto'
3
4
async function createUser({ password, name, email, phone, preference }) {
5
// create unique ID for user
6
const id = createHash('sha3-256').update(phone ? phone : email).digest('hex')
7
const key = `users:${ id }:${ email }:${ phone }`
8
const ex = 5 * 60 // expire this record in 5 minutes
9
// hash the password
10
const hashed_password = createHash('sha3-256').update(password).digest('hex')
11
await kv.set(key, { user_id: key, hashed_password, name, email, phone, preference }, { ex })
12
return key
13
}
14
15
async function findUserById(user_id) {
16
const keys = await kv.keys('users:*')
17
const key = keys.find(k => k.indexOf(user_id) >= 0)
18
return key ? await kv.get(key) : null
19
}
20
21
async function findUserByEmail(email) {
22
const keys = await kv.keys('users:*')
23
const key = keys.find(k => k.indexOf(email) >= 0)
24
return key ? await kv.get(key) : null
25
}
26
27
async function findUserByPhone(phone) {
28
const keys = await kv.keys('users:*')
29
const key = keys.find(k => k.indexOf(phone) >= 0)
30
return key ? await kv.get(key) : null
31
}
32
33
async function updatePassword(key, password) {
34
const user = await kv.get(key)
35
const ex = 5 * 60 // expire this record in 5 minutes
36
// hash the password
37
const hashed_password = createHash('sha3-256').update(password).digest('hex')
38
await kv.set(key, { ...user, hashed_password }, { ex })
39
}
40
41
export {
42
createUser,
43
findUserById,
44
findUserByEmail,
45
findUserByPhone,
46
updatePassword
47
}

Please note that the code above auto-deletes user information after 5 minutes. This is a (not so gentle) reminder that this is not designed for production.

Handling Sessions

Create a file at the root of your project called session.js and paste the following code:

1
function getSession(req, attr) {
2
const cookie = req.cookies.get(attr)
3
return (cookie ? cookie.value : undefined)
4
}
5
6
function setSession(res, attr, value) {
7
res.cookies.set(attr, value)
8
}
9
10
export {
11
getSession,
12
setSession
13
}

This code uses cookies to simulate a session. Once again, do not use this in production.

Try Creating a User

Ok, we've done a lot of work to wire up Courier, Vercel and our Next.js application. Let’s see if you can create a User! Go to http://localhost:3000/new-user, fill-out the form and click submit. You should be redirected to the index page.

Now, go back to your Courier account and click on "users" in the left nav. If all went well, you should see the new User you created!

With that out of the way, we can finally get to what you came here for: password reset notifications 📬!

Forgot Password Page

Create a directory under app called forgot-password, create a new file in it called page.js and paste the following code:

1
'use client'
2
import { useRouter } from 'next/navigation'
3
import { useState } from 'react'
4
5
async function sendToken(payload) {
6
const res = await fetch('/send-token', { method: 'POST', body: JSON.stringify(payload) })
7
if (!res.ok) return undefined
8
return res.json()
9
}
10
11
export default function ForgotPassword(request) {
12
const router = useRouter()
13
const [ error, setError ] = useState()
14
15
async function onForgotPassword(event) {
16
event.preventDefault()
17
const formData = new FormData(event.target)
18
const payload = {
19
email: formData.get('email'),
20
phone: formData.get('phone')
21
}
22
const response = await sendToken(payload)
23
if (response.error) {
24
setError(response.error)
25
}
26
else if (response.redirect) {
27
router.push(`${response.redirect}?mode=${response.mode}`)
28
}
29
return true
30
}
31
return (
32
<main className="flex min-h-screen flex-col items-center justify-between p-24">
33
<form method="post" onSubmit={ onForgotPassword } className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
34
{ error && ( <div className="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative">{ error }</div>) }
35
<div className="mb-4">Please use the same email or phone number that you used to <a href="/new-user">create your user</a>.</div>
36
<div className="mb-4">
37
<label htmlFor="email" className="block text-gray-700 text-sm font-bold mb-2">Email Address</label>
38
<input type="email" name="email" id="email" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"></input>
39
</div>
40
<div className="mb-4">- or -</div>
41
<div className="mb-4">
42
<label htmlFor="phone" className="block text-gray-700 text-sm font-bold mb-2">Mobile Phone</label>
43
<input type="text" name="phone" id="phone" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"></input>
44
</div>
45
<input type="submit" value="Reset Password" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"></input>
46
</form>
47
</main>
48
)
49
}

This code is functionality identical to the code we used for the new-user page. The more interesting code is on the backend.

Send Token Route

Create a new directory under app called send-token, create a file called route.js and paste the following code:

1
import { NextResponse } from 'next/server'
2
import { kv } from '@vercel/kv'
3
import { CourierClient } from '@trycourier/courier'
4
import { findUserByEmail, findUserByPhone } from '../../models/users'
5
import { setSession } from '../../session'
6
7
const courier = CourierClient({ authorizationToken: process.env.courier_auth_token })
8
9
export async function POST(request) {
10
// get phone number and email from form payload
11
const data = await request.json()
12
const { email, phone } = data
13
let user
14
// look up the user based on phone or email
15
if (email) {
16
user = await findUserByEmail(email)
17
}
18
else if (phone) {
19
user = await findUserByPhone(phone)
20
}
21
else {
22
// neither an email nor phone number was submitted, re-direct and display error
23
return NextResponse.json({
24
error: 'You must provide an email or phone number'
25
})
26
}
27
28
if (user) {
29
const { user_id } = user
30
// generate reset token
31
const token = Math.floor(Math.random() * 1000000).toString().padStart(6, '0')
32
const ex = 5 * 60 // expire this record in 5 minutes
33
// store in KV cache
34
await kv.set(`${user_id}:reset`, token, { ex })
35
// send notification
36
await courier.send({
37
message: {
38
to: {
39
user_id
40
},
41
template: process.env.COURIER_TEMPLATE,
42
data: {
43
token
44
}
45
}
46
})
47
// redirect to enter token page
48
return NextResponse.json({
49
redirect: '/enter-token',
50
mode: (email ? 'email' : 'phone')
51
})
52
}
53
else {
54
// redirect and display error
55
return NextResponse.json({
56
error: 'We could not locate a user with that email address or phone number'
57
})
58
}
59
}

This function uses the user model to retrieve a user based on either an email address or a phone number. A random 6 digit token is generated, stored in Vercel KV and sent to the user for verification using Courier.

Vercel KV is a durable Redis store that's great for the use case of storing tokens and auto-purging them after a few minutes. The code in our function sets a key (who’s value is the user_id appended with :reset) with the value of the token. The ex attribute specifies when the key/value should be automatically purged from the DB.

Let's take a sec to break down this Courier send API call. The top-level attribute that we pass in this API call is a message object. The message object supports several properties that you can use to send and route messages, but in this case we only use 3:

  • to - required, specifies the recipient(s) of the message
  • template - the template to use for this message
  • data - an arbitrary JSON payload of dynamic data

Since the user's email address and phone number have already been stored in the user's Courier Profile, we only need to pass a user_id in the to portion of the message. Courier will figure out whether to use the email address or phone number based on the user's preference that we stored in custom.preference.

The data attribute is where we store the token that we've generated. Values in data are interpolated into the templates that you define when the message is being rendered. Let’s switch out of the code (briefly!) to define our SMS and email notification templates.

Building the Email and SMS Notification Templates

Go back to the Courier App, and click “Designer” on the left nav. Edit the “Password Reset Token” template that you created.

For the email template, set the Subject to "Your password reset token" and add a Markdown Block to the body of the message with the following content:

1
Hello {profile.name}, here is your {token}

You should see {profile.name} and {token} be highlighted in green. This means that Courier is recognizing them as variables.

Since we set the name attribute when creating the profile, it’s magically available to us in the template. Cool! Also, since we passed a token attribute in the send API call, that is also available to us here in this template.

Click on SMS on the left to edit the SMS template. Create a Markdown Block in the body of the message and type out the following:

1
Hi there {profile.name} đź‘‹ Your password reset token is: {token}

Routing to the Right Channel

Now that we have the template content defined, we need to ensure the messages route to the correct channel based on the user's preference.

Hover your mouse over "email" on the left, and you'll see a gear icon appear. Click the gear icon to edit this channel's settings. Click "conditions" on the left and "add" a new condition.

We are going to "disable" this channel when the profile.preference property is equal to "phone":

  • Flip the enable/disable toggle to disable.
  • Select "Profile" for source.
  • Select "=" for operator.
  • Type in "phone" for value.

Once you're done entering info, everything is auto-saved. Just click outside of the modal to close it. Repeat the same process for the "sms" channel, but set the value for the conditional to "email". We have now disabled these channels in the event the user has selected a different one to receive their notifications on.

Click “publish” in the top right corner to make make these changes live.

Enter Token Page

Ok, back to the code! Create a new directory in app called enter-token and a file in it called page.js. The user is redirected to this page and must enter the token they are sent via email or SMS in order to proceed. Paste this code into the file:

1
'use client'
2
import { useRouter } from 'next/navigation'
3
import { useState } from 'react'
4
5
async function verifyToken(payload) {
6
const res = await fetch('/verify-token', { method: 'POST', body: JSON.stringify(payload) })
7
if (!res.ok) return undefined
8
return res.json()
9
}
10
11
export default function EnterToken(request) {
12
const router = useRouter()
13
const [ error, setError ] = useState()
14
15
async function onVerifyToken(event) {
16
event.preventDefault()
17
const formData = new FormData(event.target)
18
const payload = {
19
token: formData.get('token'),
20
}
21
const response = await verifyToken(payload)
22
if (response.error) {
23
setError(response.error)
24
}
25
else if (response.redirect) {
26
router.push(response.redirect)
27
}
28
return true
29
}
30
31
const mode = request.searchParams?.mode
32
return (
33
<main className="flex min-h-screen flex-col items-center justify-between p-24">
34
<form method="post" onSubmit={ onVerifyToken } className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
35
{ error && ( <div className="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative">{ error }</div>) }
36
<div className="mb-4">Check your { mode } and enter token that we have sent you below. </div>
37
<div className="mb-4">
38
<label htmlFor="token" className="block text-gray-700 text-sm font-bold mb-2">Token</label>
39
<input type="token" name="token" id="token" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"></input>
40
</div>
41
<input type="submit" value="Validate Token" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"></input>
42
</form>
43
</main>
44
)
45
}

Nothing to see here, let’s check out the server-side route that handles this form.

Verify Token Route

Create a directory in app called verify-token and a file in it called route.js with the following code:

1
import { NextResponse } from 'next/server'
2
import { kv } from '@vercel/kv'
3
import User from '../../models/users'
4
import { getSession, setSession } from '../../session'
5
6
export async function POST(request) {
7
// get phone number and email from form payload
8
const data = await request.json()
9
const { token } = data
10
// get user_id from session
11
const userId = getSession(request, 'user_id')
12
const storedToken = "" + await kv.get(`${userId}:reset`) // ensure the token is of type string
13
if (userId && token && (token === storedToken)) {
14
// redirect to reset password page
15
const response = NextResponse.json({
16
redirect: '/new-password'
17
})
18
setSession(response, 'authenticated', true)
19
return response
20
}
21
else {
22
// redirect and display error
23
return NextResponse.json({
24
error: 'Token did not match, please try again?'
25
})
26
}
27
}

Here, we get the token from the form submission and the user_id from the session. We use the user_id to construct the key, and use the key to retrieve the value stored there. We then check to see if the values of the form submission and stored tokens match. If they don't match, we return an error to the page.

If they DO match, we set a property on our session of authenticated to the value of true and forward to the /new-password page.

Locking Down Routes with Middleware

The last page we are going to build (new-password) and the last route we are going to build (update-password) should be considered secure. We don’t want users to interact with these pages unless they have authenticated themselves by successfully confirming they have received the token we sent them. A recommended way to secure pages and routes in Next.js is by using middleware.

Create a new file in the root of your project called middleware.js and paste the following code:

1
import { NextResponse } from 'next/server'
2
import { getSession } from './session'
3
4
export function middleware(request) {
5
const authenticated = getSession(request, 'authenticated')
6
if (authenticated) {
7
return NextResponse.next()
8
}
9
else {
10
const homeUrl = new URL('/', request.url)
11
homeUrl.searchParams.set('message', 'You are not authorized')
12
return NextResponse.redirect(homeUrl)
13
}
14
}
15
16
export const config = {
17
matcher: ['/new-password', '/reset-password'],
18
}

This middleware function processes every request that matches /new-password or /reset-password. For matching requests, the middleware checks the session to see if the user is authenticated. If so, it proceeds with the request. If not, it redirects to the index page with an error message.

New Password Page

Create a directory in app called new-password and a file in it called page.js with the following code:

1
'use client'
2
import { useRouter } from 'next/navigation'
3
import { useState } from 'react'
4
5
async function resetPassword(payload) {
6
const res = await fetch('/reset-password', { method: 'POST', body: JSON.stringify(payload) })
7
if (!res.ok) return undefined
8
return res.json()
9
}
10
11
export default function NewPassword(request) {
12
const router = useRouter()
13
const [ error, setError ] = useState()
14
15
async function onResetPassword(event) {
16
event.preventDefault()
17
const formData = new FormData(event.target)
18
const payload = {
19
newPassword: formData.get('new_password'),
20
newPasswordConfirm: formData.get('new_password_confirm'),
21
}
22
const response = await resetPassword(payload)
23
if (response.error) {
24
setError(response.error)
25
}
26
else if (response.redirect) {
27
router.push(`${response.redirect}?message=${response.message}`)
28
}
29
return true
30
}
31
32
return (
33
<main className="flex min-h-screen flex-col items-center justify-between p-24">
34
<form method="post" onSubmit={ onResetPassword } className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
35
{ error && ( <div className="mb-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded relative">{ error }</div>) }
36
<div className="mb-4">Almost done! Now just enter a new password.</div>
37
<div className="mb-4">
38
<label htmlFor="new_password" className="block text-gray-700 text-sm font-bold mb-2">New Password</label>
39
<input type="password" name="new_password" id="new_password" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"></input>
40
</div>
41
<div className="mb-4">
42
<label htmlFor="new_password_confirm" className="block text-gray-700 text-sm font-bold mb-2">Confirm Password</label>
43
<input type="password" name="new_password_confirm" id="new_password_confirm" className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"></input>
44
</div>
45
<input type="submit" value="Reset Password" className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"></input>
46
</form>
47
</main>
48
)
49
}

Once again, a pretty standard page with a form. Let’s look into the route that processes the new passwords.

Reset Password Route

Create a directory in app called reset-password and a file in it called route.js with the following code:

1
import { NextResponse } from 'next/server'
2
import { kv } from '@vercel/kv'
3
import { updatePassword } from '../../models/users'
4
import { getSession } from '../../session'
5
6
export async function POST(request) {
7
// get passwords from payload
8
const data = await request.json()
9
const { newPassword, newPasswordConfirm } = data
10
// get user_id from session
11
const user_id = getSession(request, 'user_id')
12
// update the user
13
if (user_id && newPassword && newPasswordConfirm && (newPassword === newPasswordConfirm)) {
14
await updatePassword(user_id, newPassword)
15
return NextResponse.json({
16
redirect: '/',
17
message: 'Your password has been reset đź‘Ť'
18
})
19
}
20
else {
21
// password don't match
22
return NextResponse.json({
23
error: 'Your passwords must match'
24
})
25
}
26
}

Here we get the new password and the confirmed password from the form and the user_id from the session. If we have a user_id and the password match, update the user’s password! Woo hoo, we did it!

Wrapping Things Up

Phew, we made it! Our goal was to use Courier and Next.js to build a secure password reset flow that allowed the user to receive either an SMS or an email based on their preferences. Let’s review what we covered in this tutorial:

  • Creating a fresh Next.js (app router) web application
  • Building Next.js client-side pages, server-side routes and middleware
  • Configuring Courier to use SMS and email providers
  • Creating templates for SMS and email notifications
  • Storing user profile and preference data in Courier
  • Using Vercel KV for token storage
  • Routing token notifications based on user preferences

I hope you enjoyed this tutorial and you can ping me (crtr0) on Twitter if you have any questions!

You can find the full source code for this application on Github. Pull requests welcome! You can also play with a live demo of this app which is hosted on Vercel.

Create Your Free Developer Account

Courier makes it easy to send SMS, email, push and in-app notifications with a single API call. We have a REST API, client SDKs and Docs that you'll love đź’ś

Sign-up

More from Tutorial

sms notification system thumbnail
TUTORIAL

How to Create an Automated SMS Notification System

This article explains why you need an automated SMS notification system, with an example using Courier to send an automated SMS with Twilio.

The Courier Team

The Courier Team

August 17, 2023

In-App Notification Inbox for Web & Mobile thumbnail
TUTORIAL

Building In-App Notifications for Web and Mobile Applications

In-app notifications are important for increasing user engagement. Here’s how a developer can implement them in web and Android applications using Courier.

Adam Searle

Adam Searle

July 13, 2023

Build your first notification in minutes

Send up to 10,000 notifications every month, for free.

Get started for free

Email & push notification

Build your first notification in minutes

Send up to 10,000 notifications every month, for free.

Get started for free

Email & push notification

Platform

Users

Content

Channels

Sending

Workflows

Preferences

Inbox

Workspaces

Observability

API Status

Changelog

© 2023 Courier. All rights reserved.