The ultimate guide to building user authentication into your Next.js application

Next.js is a popular React framework and we’re in the process of migrating our main stytch.com website to Next.js because of the developer experience and performance wins. We’re thankful to benefit from the Next.js ecosystem for our own product and wanted to open source that work. 

Building user signup and login can be a frustrating task, especially when you just want to focus on building your core product. Stytch makes it easy to build user authentication, we offer flexible SDKs and APIs so that you can own your UX and let us do the heavy lifting. 

In this tutorial, we’ll walk through building a Next.js app with Stytch for user authentication. We’ll be using this example app to show you how you can use our API integration to build user sign up and log in with SMS one-time passcodes.

Create your Next.js app

It’s simple to get an app up and running using create-next-app. You’ll need to have the Next.js cli installed if you don’t already, instructions on that here. Then you’ll want to run:

$ npx create-next-app --typescript

Now you’ll want to run the following command to install all of your dependencies:

$ npm install

Your app is now ready for you to get started. Follow the instructions printed in your terminal to cd into your app and then start it in development mode with:

$ npm run dev

Setting up Stytch

To start, you’ll need to have a Stytch account, you can sign up here. Next, you’ll need a .env.local file. You can copy our template here. For the purpose of this guide, you only need to fill out the STYTCH_PROJECT_ID and STYTCH_SECRET env variables, which you can find in the Stytch Dashboard API keys tab.

Sending and authenticating SMS One-time Passcodes(OTP)

One benefit of Next.js is that it supports creating backend API routes under the pages/api/ folder. These routes are made accessible at <your domain>/api/<route name>

In this guide we will leverage Next.js API routes and the Stytch API by using the Stytch Node client library. Start by installing the client with:

$ npm install stytch

In order to use the Stytch Node client library, your app needs to instantiate a Stytch client. In our example project, we created a utility function to do this for you under lib/loadStytch.ts, as long as you created your .env.local file above, you don’t need to do anything else here.

let client: stytch.Client;
const loadStytch = () => {
  if (!client) {
    client = new stytch.Client({
      project_id: process.env.STYTCH_PROJECT_ID || '',
      secret: process.env.STYTCH_SECRET || '',
      env: stytch.envs.test,
    });
  }

  return client;
};

Next, we created an API route called send_otp under pages/api/. This endpoint will be available at <your domain>/api/send_otp (e.g. http://localhost:3000/api/send_otp) and will take a phone number in +1XXXXXXXXXX format in the request body. Using the Stytch client, call the otps.sms.loginOrCreate method. This will send a text to the provided phone number with a OTP. The otps.sms.loginOrCreate method also returns a methodId; return and keep track of this methodId as it will be required for the authentication endpoint later. You can take a look at how we built our send_otp endpoint here.

export async function handler(req: NextApiRequest, res: NextApiResponse<Data>) {
  if (req.method === 'POST') {
    const client = loadStytch();
    const data = JSON.parse(req.body);
    try {
      const phoneNumber = data.phoneNumber.replace(/\D/g, '');

      // params are of type stytch.LoginOrCreateUserBySMSRequest
      const params = {
        phone_number: `${data.intlCode}${phoneNumber}`,
      };

      const resp = await client.otps.sms.loginOrCreate(params);
      res.status(200).json({ methodId: resp.phone_id });
    } catch (error) {
      console.log(error);
      res.status(400);
    }
  } else {
    // Handle any other HTTP method
  }
}

Finally, we created an authentication endpoint called authenticate_otp under pages/api/. This endpoint will be available at <your domain>/api/authenticate_otp and takes the methodId from the prior send_otp response and the user supplied otpInput(the six digit OTP sent to the user’s phone). Using the Stytch client, call the otps.authenticate method, if the response status is 200, the user has successfully authenticated! You can take a look at how we built out our authenticate endpoint here.

export async function handler(req: NextIronRequest, res: NextApiResponse<Data>) {
  if (req.method === 'POST') {
    const client = loadStytch();
    const data = JSON.parse(req.body);
    try {
      // params are of type stytch.LoginOrCreateUserBySMSRequest
      const params = {
        code: data.otpInput,
        method_id: data.methodId,
      };

      const resp = await client.otps.authenticate(params);
      if (resp.status_code.toString() === '200') {
        // Set session
        req.session.destroy();
        // Save additional user data here
        req.session.set('user', {
          id: resp.user_id,
        });
        await req.session.save();
        res.status(200).send({ msg: `successfully authenticated ${resp.user_id}` });
      } else {
        throw Error('Error authenticating your code');
      }
    } catch (error) {
      console.log(error);
      res.status(400).json({ msg: error });
    }
  } else {
    // Handle any other HTTP method
  }
}

Now that you’ve set up your authentication API routes, it’s time to build a UI!

Building your login page

In addition to API routes, Next.js also automatically converts all remaining files under the /pages directory into page routes in your application. To add your login page, directly edit the pages/index.tsx file.

For your login UI, you will want to create two separate forms. One for the user to enter their phone number (which will send them an OTP), and the other for the user to enter and verify their OTP. You can take a look at the examples we’ve built for both at SendOTPForm.tsx and VerifyOTPForm.tsx. Here’s an example of how you can tie these two forms together for your log in flow, see LoginWithSMS.tsx:

const LoginWithSMS = () => {
  const [otpSent, setOTPSent] = useState(false);
  const [phoneNumber, setPhoneNumber] = useState('');
  const [methodId, setMethodId] = useState('');

  return (
    <StytchContainer>
      {!otpSent ? (
        <SendOTPForm
          phoneNumber={phoneNumber}
          setMethodId={setMethodId}
          setOTPSent={setOTPSent}
          setPhoneNumber={setPhoneNumber}
        />
      ) : (
        <VerifyOTPForm methodId={methodId} phoneNumber={phoneNumber} />
      )}
    </StytchContainer>
  );
};

Finally, you will want to create a logged-in page that the VerifyOTPForm can push to once a user has been successfully authenticated. You can see our logged-in view here and what it looks like below:

Logged-in view

Once you’ve created the login UI and your logged-in page, you are now ready to hook up your components to your API routes. Below is a diagram of what the user authentication flow should look like:

User authentication flow

Congratulations! You’ve successfully used the Stytch API in Next.js to build user sign up and log in with SMS OTP.

What’s next?

Stytch is a platform for user infrastructure, we offer many more options when it comes to your user authentication. Explore more ways to authenticate, including email or embeddable magic links, email or WhatsApp OTP, and OAuth in our Docs!