16 min readUpdated Oct 6, 2023

Building a realtime chat app with Next.js and Vercel

Building a realtime chat app with Next.js and Vercel
Devin RaderDevin Rader

This post will walk through the creation of a realtime chat application with Next.js and deploying it to Vercel.

You'll learn how to:

  • Create a brand new Next.js application
  • Adding real-time functionality with Ably
  • Create a Next.js Vercel Serverless API
  • Use React Functional components and React Hooks with Ably
  • Host your app on Vercel

Check out a running version of the app or fork the app on GitHub.

WebSockets in Vercel with Ably

Vercel allows users to deploy Serverless Functions, which are essentially just blocks of code that provide a response to an HTTP request. However, these functions have a maximum execution timeout, which means that it is not possible to maintain a WebSocket connection this way.

This is where Ably comes in. The client can connect to an Ably Channel and send and receive messages on it to add realtime functionality to your app by managing your WebSocket connections for you.

We’ll start by going over how to build an app that uses realtime functionality. If you prefer, you can jump straight to how to use Ably with Vercel.

Take a look at some of the challenges of implementing a reliable and highly-performant client-side WebSocket solution for JavaScript apps.

What are we going to build?

We’re building a realtime chat app that runs in the browser. It will be built upon the Next.js create-next-app template and contain a React component that uses Ably to send and receive messages. We'll also write a Next.js serverless function that will be used to connect to Ably.

The UI of the chat app we'll build. It is a window with speech bubbles for text.

Dependencies

To build this app, you’ll need:

Once you have your Ably account you'll need an API key to authenticate with the Ably Service. To get an API key:

  1. Visit your app dashboard and click on "Create New App".
  2. Give the new app a name.
  3. Copy the Private API key once the app has been created. Keep it safe, this is how you will authenticate with the Ably service.

Vercel provides Next.js command line tools to help us. They don't need to be installed on your system as they're executed using npx.

Scaffolding the Next.js Chat App

Start building the chat application by using create-next-app to scaffold a new Next.js-based application. In your terminal, type:

npx create-next-app@latest

Answer the prompts as follows:

What is your project named? nextjs-chat-app
Would you like to use TypeScript? No
Would you like to use ESLint? Yes
Would you like to use Tailwind CSS? No
Would you like to use `src/` directory? No
Would you like to use App Router? (recommended) Yes
Would you like to customize the default import alias? No
What import alias would you like configured? @/*

After the prompts, create-next-app will create a folder with your project name and install the required dependencies. Change directories into your application directory:

cd nextjs-chat-app

Next, create a file called .env in the root of the directory. This is where we'll put the project's environment variables. Add your Ably API key to the .env file:

ABLY_API_KEY=your-ably-api-key:goes-here

Test that the application runs correctly by running the following command:

npm run dev

The Next.js dev server will spin up and you'll see an default Next.js starter app. This is what we'll build our chat app on top of.

Realtime pub/sub messaging with Ably

The chat app will use Ably for Publish/Subscribe (Pub/Sub messaging between the users. Pub/Sub is a popular pattern used for realtime data delivery. The app will be able to send, or publish, messages over an Ably Channel. Clients that use the app will be subscribed to the channel and able to receive messages. We'll build a UI to create messages to be sent and to display messages as they are received.

Try our APIs for free

The realtime chat app architecture

The topology of our Next.js app will look like this:

├─ .env
├─ .gitignore
├─ package-lock.json
├─ package.json
├─ README.md   
|    
├─── components
│     ├─ Chat.jsx
|     ├─ ChatBox.jsx
│     ├─ ChatBox.module.css
|
├─── app
│    ├─ globals.css
│    ├─ layout.js
│    ├─ page.js
│    │   
│    └─── api
│          └─ route.js
│           
└─── public
  • /app/page.js is the home page
  • /app/api/route.js is our Ably token authentication API
  • /components/Chat.jsx is the main chat host component
  • /components/ChatBox.jsx the component that contains the actual chat UI
  • /components/ChatBox.module.css contains the styles for the chat component

Let's walk through how this application is built.

Authenticating an Ably client

There are a number of ways to authenticate an Ably client. For our application, we’ll use token authentication. Token authentication uses a trusted device with an API key, in our case an API route that we’ll build using Next.js, to issue time-limited tokens to untrusted clients.

In version 13, Next.js introduced the new App Router. Built on React Server Components it supports shared layouts, nested routing, loading states, error handling, and more including API routes via its Route Handler feature. Let's build that API route.

Start by installing the Ably npm package in the root of your new app:

npm install ably

Next, create a file called ./app/api/route.js. Add the following code:

import Ably from "ably/promises";

export async function GET(request) {
    const client = new Ably.Realtime(process.env.ABLY_API_KEY);
    const tokenRequestData = await client.auth.createTokenRequest({ clientId: 'ably-nextjs-demo' });
    return Response.json(tokenRequestData);
};

This function uses the Ably SDK to create a tokenRequest with your API key. The token will be used later - it allows you to keep your "real" API key safe while using it in the Next.js app. By default, this API is configured to be available on http://localhost:3000/api/. We're going to provide this URL to the Ably SDK in our client to authenticate with Ably.

Next.js Server vs Client Components

Before we start creating pages in our application, it's important to understand how Next.js renders content. The framework supports multiple rendering methods including server-side rendering (SSR), static site rendering (SSG), and client-side rendering (CSR). There are many pros and cons to each rendering method (too many to cover in this post) so if these concepts are new to you, Google’s web.dev site has a very good introduction to rendering on the web that can help you understand rendering options.

Next.js defaults to server-side rendering meaning the page HTML is generated on the server for each request and sent to the browser. This means that by default when you create a new component in your React app, you are creating a Server Component.

Server Components offer a lot of benefits, but one drawback is that they don’t have access to browser APIs. To allow you to create components that can access those APIs Next.js also includes the ability to create Client Components.

To indicate that you want a component to be a Client Component, you use React’s use client directive at the top of your component. Doing this tells React that it should render the component on the client side.

When using Next.js however even for components marked with use client the framework will try to render a static preview of the component on the server. This can cause problems when creating a component that attempts to instantiate an instance of an Ably client.

To avoid this, you can use Lazy Loading to defer loading of Client Components.

Building the components

Pages in Next.js are React components, so the app/page.js home page is the React component that contains the page layout.

Open page.js and replace the default scaffolded content with this markup:

import Head from 'next/head';

export default function Home() {
  return (
    <div className="container">
      <Head>
        <title>Realtime Chat App with Ably, NextJS and Vercel</title>
        <link rel="icon" href="https://static.ably.dev/motif-red.svg?nextjs-vercel" type="image/svg+xml" />
      </Head>
      <main>
        <h1 className="title">Next.js Chat Demo</h1>
        {/* Insert Chat Component */}
      </main>
      <footer>
        Powered by&nbsp;<a href="https://vercel.com" target="_blank" rel="noopener noreferrer">Vercel</a>&nbsp;and&nbsp;<a href="https://ably.com" rel="noopener noreferrer">Ably</a>
        <a href="https://github.com/ably-labs/NextJS-chat-app" className="github-corner" aria-label="View source on GitHub">
          <svg width="80" height="80" viewBox="0 0 250 250" className="svg" aria-hidden="true">
            <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
            <path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" className="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" className="octo-body"></path></svg>
            </a>
      </footer>
    </div>
  )
}

Next, in the same app directory, locate the globals.css file and replace its contents with the basic CSS styles we’ll use in our application which you can find in the projects GitHub repository.

Run the app. You should see this:

Next, in the root of your application create a new components folder and within create a new React component file named Chat.jsx. Scaffold the initial component using the code below:

'use client'

import * as Ably from 'ably';
import { AblyProvider } from 'ably/react';

export default function Chat() {

  const client = Ably.Realtime.Promise({ authUrl: '/api' })

  return (
    <AblyProvider client={ client }>
    </AblyProvider>
  )
}

This component is marked with the use client directive. It will be the boundary between SSR components and CSR components.

The component imports the Ably and Ably React paths from the Ably package and uses them to create a new instance of an Ably client, authenticating it against the API endpoint we created above and then providing that instance to an <AblyProvider> component. <AblyProvider> will retain the client in the application context so that any child component can access it.

To use our new Chat component we’ll dynamically import it into our home page at the top of page.js:

import Head from 'next/head';
import dynamic from 'next/dynamic';

const Chat = dynamic(() => import('../components/Chat'), {
  ssr: false,
})

And add the component to the markup:

<main>
  <h1 className="title">Next.js Chat Demo</h1>
  <Chat />
</main>

Validate that the application works properly by running the project again. You won’t see any difference in the UI yet, but we’ll get to that in the next section.

Writing the chat component logic

The chat app logic is contained inside a new ChatBox component. To create this start by creating two new files: ChatBox.jsx which will hold the React component and ChatBox.module.css, a stylesheet we’ll use to style the ChatBox component.

As we did above with globals.css you can get the CSS needed to style the ChatBox component by copying into it the styles which you can find in the projects Github repository.

Next, open the Chatbox.jsx file and add the import statements we'll need at the top of the file:

import React, { useEffect, useState } from 'react';
import { useChannel } from "ably/react";
import styles from './ChatBox.module.css';

Define a ChatBox function that is exported as a React component. Add to the function variables to store references to some of the HTML elements in the code.

export default function ChatBox() {

  let inputBox = null;
  let messageEnd = null;

  return ()
}

Set up the state properties used in the component:

const [messageText, setMessageText] = useState("");
const [receivedMessages, setMessages] = useState([]);     
const messageTextIsEmpty = messageText.trim().length === 0;
  • messageText will be bound to a textarea element where messages can be typed
  • receiveMessages stores the on-screen chat history
  • messageTextIsEmpty is used to disable the send button when the textarea is empty

Now we'll use the useChannel hook that we imported earlier.
useChannel is a react-hook API for subscribing to messages from an Ably channel. You provide it with a channel name and a callback to be invoked whenever a message is received.

Both the channel instance and the Ably JavaScript SDK instance are returned from useChannel.

const { channel, ably } = useChannel("chat-demo", (message) => {
  const history = receivedMessages.slice(-199);
  setMessages([...history, message]);
});

Here we're computing the state that'll be drawn into the message history. We do that by slicing the last 199 messages from the receivedMessages buffer.

Then we take the message history and combine it with the new message. This means we'll always have up to 199 messages + 1 new message, stored using the setMessages React useState hook.

Next, we need to handle the UI interactions by defining a few functions.

First, there's sendChatMessage, which is responsible for publishing new messages. It uses the Ably Channel returned by the useChannel hook, clears the input, and focuses on the textarea so that users can type more messages:

const sendChatMessage = (messageText) => {
  channel.publish({ name: "chat-message", data: messageText });
  setMessageText("");
  inputBox.focus();
}

Then handleFormSubmission, which is triggered when the submit button is clicked and calls sendChatMessage, along with preventing a page reload:

const handleFormSubmission = (event) => {
  event.preventDefault();
  sendChatMessage(messageText);
}

Finally the handleKeyPress event is wired up to make sure that if a user presses the enter key, while there is text in the textarea, the sendChatMessage function is triggered.

const handleKeyPress = (event) => {
  if (e.charCode !== 13 || messageTextIsEmpty) {
    return;
  }
  sendChatMessage(messageText);
  event.preventDefault();
}

Next, we need to construct the UI elements to display the messages. To do this, we will map the received Ably messages into HTML span elements:

const messages = receivedMessages.map((message, index) => {
  const author = message.connectionId === ably.connection.id ? "me" : "other";
  return <span key={index} className={styles.message} data-author={author}>{message.data}</span>;
});

In order to keep the message box scrolled to the most recent message (the one on the bottom) we'll need to add an empty div element into the message container, which will then be scrolled into view whenever the components re-renders. This is the element that we'll add to the UI later:

<div ref={(element) => { messageEnd = element; }}></div>

We use a useEffect hook along with scrollIntoView() to scroll the message history to the bottom whenever the component renders.

useEffect(() => {
messageEnd.scrollIntoView({ behaviour: "smooth" });
});

Finally, we will write the React component markup with the event handlers all bound to the onChange and onKeyPress events in JSX.

The markup itself is just a few div elements and a form with a textarea for user input. There are two calls to the react ref function, which allows us to capture a reference to the elements when they are rendered so that we can interact with them in JavaScript. The returned markup will look like this:

return (
  <div className={styles.chatHolder}>
    <div className={styles.chatText}>
      {messages}
      <div ref={(element) => { messageEnd = element; }}></div>
    </div>
    <form onSubmit={handleFormSubmission} className={styles.form}>
      <textarea
        ref={(element) => { inputBox = element; }}
        value={messageText}
        placeholder="Type a message..."
        onChange={e => setMessageText(e.target.value)}
        onKeyPress={handleKeyPress}
        className={styles.textarea}
      ></textarea>
      <button type="submit" className={styles.button} disabled={messageTextIsEmpty}>Send</button>
    </form>
  </div>
  )

Add the ChatBox component to the Chat component

'use client'

import * as Ably from 'ably';
import { AblyProvider } from 'ably/react';
import ChatBox from './ChatBox.jsx';

const client = Ably.Realtime.Promise({ authUrl: '/api' })

export default function Chat() {
  return (
    <AblyProvider client={ client }>
      <ChatBox />
    </AblyProvider>
  )
}

Swap over to your browser (make sure you restart the application server if you need to) and you should see a functioning chat application now.

Hosting on Vercel

We're using Vercel as our development server and build pipeline.

The easiest way to deploy Next.js to production is to use the Vercel platform from the creators of Next.js. Vercel is an all-in-one platform with Global CDN supporting static & Jamstack deployment and Serverless Functions.
-- The Next.js documentation

In order to deploy your new chat app to Vercel you'll need to:

  1. Create a GitHub account (if you don't already have one)
  2. Push your app to a GitHub repository
  3. Create a Vercel account
  4. Create a new Vercel app and import your app from your GitHub repository. (This will require you to authorize Vercel to use your GitHub account)
  5. Add your ABLY_API_KEY as an environment variable
  6. Watch your app deploy
  7. Visit the newly created URL in your browser!

Make it your own

There are a few ways that this example could be extended:

Add message history

There is currently no chat history in this demo, you'll only see messages that come in after you join the chat. You could expand this demo by using Ably's rewind feature for up to two minutes of history for free, or with a paid account, for up to ~48 hours.

Add user names

There aren't any usernames sent with the chat messages. This demo could be extended to introduce a username input box, and to add the current username to messages as they're sent.

The demo uses the randomly generated Ably client Id as a unique identifier - which is how it can detect if it is "me" or "someone else" who sent the message.

Let us know

Hope you have enjoyed this tutorial. If you are interested in learning more about realtime in Next.js, check out these resources:

If this tutorial was helpful, or you're using Next.js and Ably in your project, we'd love to hear about it. Drop us a message on Twitter or email us at [email protected].

Join the Ably newsletter today

1000s of industry pioneers trust Ably for monthly insights on the realtime data economy.
Enter your email