DEV Community

Cover image for ​​Add video chat to a Next.js app in 30 minutes with Daily Prebuilt
Kimberlee Johnson for Daily

Posted on • Originally published at daily.co

​​Add video chat to a Next.js app in 30 minutes with Daily Prebuilt

With the launch of our new docs site, we’ve been spending a lot of time in Next.js. We even got a little meta and embedded a Daily Prebuilt demo, built on Next, into the docs site, also built on Next.

Three participants on a video call embedded within another web page prompting for video call test

The demo lets readers quickly test out Daily calls, and get a sense of what Daily Prebuilt would look like embedded in their own app, right on the docs site. Our docs use Next API routes to create temporary Daily rooms dynamically server-side.

Since our docs codebase isn’t currently public, this post uses our /examples/prebuilt/basic-embed repository as a template to show how you can do the same in any Next app. We’ll cover:

  • Setting up the repository locally
  • Using Next API routes to create Daily rooms dynamically server-side
  • Creating a Daily callframe and joining a call once we have a room

You’ll need a Daily account if you don’t have one already.

Skip to API routes if you already have a Next project that you want to add video chat to, or if you’d rather run create-next-app to start a new app from scratch.

Set up the demo repository

Clone the /examples repo, and cd examples/prebuilt/basic-embed.

Create an .env based on .env.example, adding your Daily domain (you set this up when you created an account) and API key (you can find this in the "Developers" tab in the Daily dashboard):

DAILY_DOMAIN="your-domain"
DAILY_API_KEY="Daily API Key"
Enter fullscreen mode Exit fullscreen mode

Once you’ve added your own values, run the following from inside /basic-embed to install dependencies and start the server:

yarn 
yarn workspace @prebuilt/basic-embed dev
Enter fullscreen mode Exit fullscreen mode

You should now be able to click "Create room and start" and jump into a Daily Prebuilt call:

Clicking on create room and start on a web page starts a call with one video participant

Let’s look at how that all works.

Use API routes to create Daily video rooms dynamically server-side

Our /pages directory is where most of the fun happens. Next pages are React components. They’re associated with routes based on their file names, and come with some other neat built-in features.

For example, files inside pages/api are treated like API endpoints. These are Next API routes. In development, they are served by your dev servers, but in prod in our demo app they’ll get converted into Vercel functions, technically making them serverless.

Football coach from Ted Lasso Coach Beard raises hands over head excitedly like mind is blown

In our app, we use a Next API route to create Daily rooms:

// pages/api/room/index.js 

export default async function handler(req, res) {
 if (req.method === 'POST') {
   const options = {
     method: 'POST',
     headers: {
       Accept: 'application/json',
       'Content-Type': 'application/json',
       Authorization: `Bearer ${process.env.DAILY_API_KEY}`,
     },
     body: JSON.stringify({
       properties: {
         enable_prejoin_ui: true,
         enable_network_ui: true,
         enable_screenshare: true,
         enable_chat: true,
         exp: Math.round(Date.now() / 1000) + 300,
         eject_at_room_exp: true,
       },
     }),
   };

   const dailyRes = await fetch(
     `${process.env.DAILY_REST_DOMAIN}/rooms`,
     options
   );

   const response = await dailyRes.json();

   if (response.error) {
     return res.status(500).json(response.error);
   }

   return res.status(200).json(response);
 }

 return res.status(500);
}
Enter fullscreen mode Exit fullscreen mode

All requests to /room are handled here, and we’re specifically adding a case to handle a POST request. The request references both the Daily API key and base REST domain in the .env.

We send this request in the <CreateRoomButton /> component. This component is a button that onClick creates a room:

// components/CreateRoom.js

return (
     <Button onClick={createRoom} disabled={isValidRoom}>
       Create room and start
     </Button>
 );
Enter fullscreen mode Exit fullscreen mode

createRoom() sends a request to the Next /api/room endpoint, which makes the Daily endpoint POST request in api/room/index described above:

// components/CreateRoom.js

const createRoom = async () => {
   try {
     const res = await fetch('/api/room', {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',
       },
     });
    // Abridged snippet 
 };
Enter fullscreen mode Exit fullscreen mode

When that request resolves, it returns the Daily response object, including a url value. createRoom() sets the room value stored in local state to the response object’s url:

// components/CreateRoom.js

const resJson = await res.json();
setRoom(resJson.url);
Enter fullscreen mode Exit fullscreen mode

Now that we have a room, we’re ready for a callframe.

Create a Daily callframe and join a call

Our <Call /> component not only renders <CreateRoom />, but also initializes the callframe with a useEffect hook:

// components/Call.js

useEffect(() => {
   if (callFrame) return;

   createAndJoinCall();
 }, [callFrame, createAndJoinCall]);
Enter fullscreen mode Exit fullscreen mode

The hook calls createAndJoinCall(), a function that:

  • Creates a new Daily callframe, embedding it in the ref we identified <div ref={callRef} className="call" /> and passing along some properties we stored in the CALL_OPTIONS constant
  • Joins the Daily room using the room value stored in local state
    • Listens for the 'left-meeting' event so it can reset app state when the local participant leaves the call
// components/Call.js 

const createAndJoinCall = useCallback(() => {
   const newCallFrame = DailyIframe.createFrame(
     callRef?.current,
     CALL_OPTIONS
   );

   setCallFrame(newCallFrame);

   newCallFrame.join({ url: room });

   const leaveCall = () => {
     setRoom(null);
     setCallFrame(null);
     callFrame.destroy();
   };

   newCallFrame.on('left-meeting', leaveCall);
 }, [room, setCallFrame]);
Enter fullscreen mode Exit fullscreen mode

createAndJoinCall() is invoked whether a room is created dynamically in real-time, as we walked through in the <CreateRoom /> component, or a room is submitted through the input rendered in <Home />:

// components/Home.js 

<Field label="Or enter room to join">
    <TextInput
        ref={roomRef}
        type="text"
        placeholder="Enter room URL..."
        pattern="^(https:\/\/)?[\w.-]+(\.(daily\.(co)))+[\/\/]+[\w.-]+$"
        onChange={checkValidity}
     />
</Field>
Enter fullscreen mode Exit fullscreen mode

The input calls checkValidity() as its values change. This function makes sure that the entered text is a valid Daily room URL based on the pattern value, and sets the local state value isValidRoom to true if so:

// components/Home.js 

const checkValidity = useCallback(
   (e) => {
     if (e?.target?.checkValidity()) {
       setIsValidRoom(true);
     }
   },
   [isValidRoom]
 );
Enter fullscreen mode Exit fullscreen mode

This enables the "Join room" button:

// components/Home.js 

<Button onClick={joinCall} disabled={!isValidRoom}>
     Join room
</Button>
Enter fullscreen mode Exit fullscreen mode

Clicking the button calls joinCall(), which sets the room value stored in local state to the input:

// components/Home.js

const joinCall = useCallback(() => {
   const roomUrl = roomRef?.current?.value;
   setRoom(roomUrl);
 }, [roomRef]);
Enter fullscreen mode Exit fullscreen mode

The room value in local state triggers the callframe creation in <Call /> in the same way it did when we created a room dynamically. In both cases a room value also instructs index.js to display the <Call /> instead of the <Home /> component, according to this ternary statement:

// pages/index.js      

<main>
       {room ? (
         <Call
           room={room}
           expiry={expiry}
           setRoom={setRoom}
           setCallFrame={setCallFrame}
           callFrame={callFrame}
         />
       ) : (
         <Home
           setRoom={setRoom}
           setExpiry={setExpiry}
           isConfigured={isConfigured}
         />
       )}
     </main>
Enter fullscreen mode Exit fullscreen mode

thank u, Next.js

That’s the core of the app! There are a few other tangential things in the codebase that we didn’t get into, like the <ExpiryTimer />component and how we put [getStaticProps()`](https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation) to work checking for env variables, but we welcome you to explore those things yourself and ping us with questions. Or, if you’d rather build your own video chat interface with Next.js, check out our post using Next with the Daily call object.

More resources

Discussion (0)