Building a waitlist for your product with Next.js | Blog | PropelAuth

Building a waitlist allows your future users to express interest in you, before you’ve even started your MVP. You can see if your messaging resonates with potential customers, and when you are ready to launch, the users from your waitlist will make excellent early product testers.

In this post, we’ll build the following Next.js application:

Creating our Next.js Application

Creating a blank project

Use create-next-app to set up a new project, and then yarn dev to run it.

$ npx create-next-app@latest waitlist
$ cd waitlist
$ yarn dev

I like to start with a blank project, so let’s replace the existing code in pages/index.js with this:

import Head from 'next/head'
import styles from '../styles/Home.module.css'

export default function Home() {
    return (
        <div className={styles.container}>
            <Head>
                <title>Waitlist</title>
                <meta name="description" content="A quick, scalable waitlist"/>
                <link rel="icon" href="/favicon.ico"/>
            </Head>
        </div>
    )
}

We can also delete everything in styles/Home.module.css, we’ll replace it shortly. If you go to http://localhost:3000, you’ll see a blank page with Waitlist as the title.

Creating a two column layout

As you saw before, we want a classic two column layout with an image on the right and some marketing text on the left. We’ll use a flexbox layout. Add the following to your styles/Home.module.css.

.container {
    background-color: #293747; /* background color */
    min-height: 100vh;         /* cover at least the whole screen */
    height: 100%;
    display: flex;             /* our flex layout */
    flex-wrap: wrap;
}
.column {
    flex: 50%;                 /* each column takes up half the screen */
    margin: auto;              /* vertically align each column */
    padding: 2rem;
}
@media screen and (max-width: 600px) {
    .column {
        flex: 100%;
    }
}

Back in pages/index.js, we will add two components for the left and right columns. On the right side, we’ll put an image of some code.

// ...
            <Head>
                <title>Waitlist</title>
                <meta name="description" content="A quick, scalable waitlist"/>
                <link rel="icon" href="/favicon.ico"/>
            </Head>

// New components
            <LeftSide/>
            <RightSide/>
        </div>
    )
}

function LeftSide() {
    return <div className={styles.column}>
        Hello from the left side
    </div>
}

function RightSide() {
    return <div className={styles.column}>
        <img width="100%" height="100%" src="/code.svg"/>
    </div>
}

The right side looks great! It covers the right half of the screen like we expected. The left side, however, is pretty ugly and unreadable. Let’s address that now.

Formatting our marketing text

We know what we want our LeftSide to say, let’s start by updating it so the text matches our image above. For now, we’ll also put in placeholder styles which we will add afterwards.

function LeftSide() {
    return <div className={styles.column}>
        <img width="154" height="27" src="/logo.svg"/>
        <h1 className={styles.title}>
            Quick Scalable<br/>
            <span className={styles.titleKeyword}>Waitlist</span>
        </h1>
        <div className={styles.subtitle}>
            Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore
            et dolore magna aliqua.
        </div>
    </div>
}

If it wasn’t for the bad contrast between the black text and the background, this wouldn’t look too bad. Now we can add the title, titleKeyword, and subtitle classes (in styles/Home.module.css) to clean it up.

.title {
    font-size: 4rem;
    color: white;
}
.titleKeyword {
    color: #909aeb;
}
.subtitle {
    font-size: 1.2rem;
    font-weight: 250;
    color: white;
}

Adding the waitlist form

Our frontend is really coming together. The only remaining part is the form where the user can submit their email address. We’ll place this in a separate component called Form and add it to the bottom of our LeftSide component.

function LeftSide() {
    return <div className={styles.column}>
        <Form />
    </div>
}

function Form() {
    const [email, setEmail] = useState("");
    const [hasSubmitted, setHasSubmitted] = useState(false);
    const [error, setError] = useState(null);

const submit = async (e) => {
        e.preventDefault();

// TODO: make a POST request to our backend
    }

if (hasSubmitted) {
        return <div className={styles.formWrapper}>
            <span className={styles.subtitle}>
                Thanks for signing up! We will be in touch soon.
            </span>
        </div>
    }

return <form className={styles.formWrapper} onSubmit={submit}>
        <input type="email" required placeholder="Email"
               className={[styles.formInput, styles.formTextInput].join(" ")}
               value={email} onChange={e => setEmail(e.target.value)}/>

<button type="submit" className={[styles.formInput, styles.formSubmitButton].join(" ")}>  
            Join Waitlist
        </button>

{error ? <div className={styles.error}>{error}</div> : null}
    </form>
}

Making a request to a Next.js API route

Our design is finished! Now all we have to do is make sure when you click submit that two things happen:

  1. The frontend makes a request to our backend with the email address
  2. The backend saves the email address somewhere

Here’s our finished submit method:

const submit = async (e) => {
    e.preventDefault();
    let response = await fetch("/api/waitlist", {
        method: "POST",
        body: JSON.stringify({email: email})
    })
    if (response.ok) {
        setHasSubmitted(true);
    } else {
        setError(await response.text())
    }
}

Creating a Next.js API route

Creating an empty route

Our blank application actually started with an API route in /pages/api/hello.js which looks like this:

export default function handler(req, res) {
  res.status(200).json({ name: 'John Doe' })
}

We can delete hello.js and make a new file /pages/api/waitlist.js:

import validator from "email-validator";

export default async function handler(req, res) {
    if (req.method === 'POST') {
        await postHandler(req, res);
    } else {
        res.status(404).send("");
    }
}

async function postHandler(req, res) {
    const body = JSON.parse(req.body);
    const email = parseAndValidateEmail(body, res);
    await saveEmail(email);
    res.status(200).send("")
}

async function saveEmail(email) {
    console.log("Got email: " + email)
}

function parseAndValidateEmail(body, res) {
    if (!body) {
        res.status(400).send("Malformed request");
    }

const email = body["email"]
    if (!email) {
        res.status(400).send("Missing email");
    } else if (email.length > 300) {
        res.status(400).send("Email is too long");
    } else if (!validator.validate(email)) {
        res.status(400).send("Invalid email");
    }

return email
}

How to persist waitlist emails

While logging the email is fine, you are probably going to want something more durable. If you don’t expect a lot of users and are using Slack, you can use a Webhook integration to send a message to slack every time a user signs up.

const { IncomingWebhook } = require('@slack/webhook');
const url = process.env.SLACK_WEBHOOK_URL;

async function saveEmail(email) {
    const webhook = new IncomingWebhook(url);
    await webhook.send({
        text: 'New waitlist request: ' + email,
    });
}

You could also save it to a database. For example, if you are using CockroachDB:

import { Pool, Client } from 'pg'

const connectionString = process.env.DB_CONNECTION_STRING;

async function saveEmail(email) {
    try {
        const client = new Client({connectionString})
        await client.connect()

const query = 'INSERT INTO waitlist(email) VALUES($1)'
        const values = [email]

await client.query(query, values)
        await client.end()
    } catch (err) {
        console.log(err.stack)
        res.status(503).send("An unexpected error has occurred, please try again");
    }
}

Extra features

This waitlist is pretty easy to extend. You may, for example, want to:

Ultimately, the important thing is you are getting the information you need from your future users, and you are saving it durably.

Next steps

After building out your waitlist, you will probably begin to build out an MVP of your product. You can speed up that process by using PropelAuth - a hosted authentication service.