Use Next API routes and Daily to create video chat rooms dynamically

2022-04-07: The code snippets in this post reference an older version of the examples repo prior to the release of Daily React Hooks. Any code not using Daily React Hooks is still valid and will work; however, we strongly suggest used Daily React Hooks in any React apps using Daily. To see the pre-Daily hooks version of the examples repo, please refer to the pre-daily-hooks branch. To learn more about Daily React Hooks, read our announcement post.

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 in a website under a Demos section

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”

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

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

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 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);
}

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/CreateRoomButton.js
 
return (
     <Button onClick={createRoom} disabled={isValidRoom}>
       Create room and start
     </Button>
 );

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

// components/CreateRoomButton.js

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

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/CreateRoomButton.js
 
const resJson = await res.json();
setRoom(resJson.url);

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 <CreateRoomButton />, but also initializes the callframe with a useEffect hook:

// components/Call.js
 
useEffect(() => {
   if (callFrame) return;
 
   createAndJoinCall();
 }, [callFrame, createAndJoinCall]);

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]);

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>

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]
 );

This enables the "Join room" button:

// components/Home.js 
 
<Button onClick={joinCall} disabled={!isValidRoom}>
     Join room
</Button>

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]);

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>

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() 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

Never miss a story

Get the latest direct to your inbox.