ProNextJS
    Loading
    lesson

    Adding Parameterized Routes to a Next.js App

    Jack HerringtonJack Herrington

    Parameterized routes allow the user to load data based on a specific parameter in the URL. For example, /chats/id would point to a chat with the given id, and load its data.

    Let's implement this feature, starting with updating the Chat component.

    Preparing the Chat Component

    To get started, we'll need to expand our chat component's capabilities since we're planning to use it on both the homepage to initiate chats and on the route to the chat specified by the chatId.

    The default id will be null, and the messages will be initialized with the initialMessages from the current chat or an empty array:

    export default function Chat({
      id = null,
      messages: initialMessages = [],
    }: {
      id?: number | null;
      messages?: Message[];
    }) {
      const [messages, setMessages] = useState<Message[]>(initialMessages);
      ...
    

    Now we'll move on to creating the route.

    Creating a Parameterized Route and Component

    Navigate back to the app directory and create a new file chats/[chatId]/page.tsx. The chatId will be the parameter we're interested in. We're going to use the page.tsx extension because we plan to include a React component here.

    chats/[chatId]/page.tsx
    

    In order to get the chatId from the URL, we need to have the ChatDetail component use the params property. This special property contains a list of params, which in this case is the chatId as a string. If you had multiple parameters, they would each be additional keys on the params object.

    Here's the start of the ChatDetail component:

    // inside chats/[chatId]/page.tsx
    
    export default async function ChatDetail({
      params: { chatId },
    }: {
      params: { chatId: string };  
    }) {
      // component code will go here
    
    }
    

    Displaying the Chat

    Now, we have to import our Chat component because we're planning to display it, and we'll wrap it in a main tag.

    We will have the chatId, so we will need to use getChat from the database to retrieve the chat:

    // inside chats/[chatId]/page.tsx
    import Chat from "@/app/components/Chat";
    import { getChat } from "@/db";
    

    Inside the ChatDetail component, we can use getChat to retrieve the chat details. Since we're in a React server component, we can use await to get the chat details directly. Note the use of the + operator to convert the chatId to a number:

    export default async function ChatDetail({
      params: { chatId },
    }: {
      params: { chatId: string };  
    }) {
      const chat = await getChat(+chatId);
    
      return (
        <main className="pt-5">
          <Chat id={+chatId} key={chatId} />
        </main>
      );
    }
    

    With the component for our parameterized route in place, we can now test it.

    Testing the Route

    In the browser, navigate to the URL for a specific chat room, such as /chats/3. If the chat exists, you should see the previous messages displayed.

    However, when adding a message you might see an error that additional properties are not allowed:

    Error: 400 Additional Properties are not allowed

    The issue is that OpenAI is receiving extra data on messages. Remember, we're retrieving messages from the database, and these messages come with extra fields such as chat_id and id.

    What OpenAI needs, however, is just the role and content. The system is being quite strict about this - while you might assume it could just ignore any additional fields it doesn't need, it seems we actually need to trim down our output to include just what OpenAI expects.

    To fix the error, we need to make a slight modification to our getCompletion function. We want to adjust it to accommodate OpenAI's requirements by only passing the role and content fields.

    Inside of getCompletion, we'll update the messages object to only include the role and content fields:

    // inside getCompletion.ts 
    const messages = [
      ...messageHistory,
      response.choices[0].message as unknown as {
        role: "user" | "assistant";
        content: string;
      },
    ];
    

    Upon making these changes, you should be able to successfully calculate and return the response for a simple computation such as "What is 70*80?"

    Checking in the database client in the terminal, we can see the that the new message has been added to the chat as expected.

    But if we refresh the page in the browser, our new message isn't displayed!

    Dealing with Next.js Route Cache

    We don't see our new messages because Next.js aggressively caches its routes.

    What this means is that Next.js assumes that the data related to any route once it's fetched can be stored and served again if requested. This poses a problem in our scenario because chat data is going to keep changing.

    There are a couple of techniques for dealing with this issue, but for now we'll just tell Next.js that this is a dynamic page. This will force Next.js to re-render the component and fetch fresh content each time it's requested.

    Back in app/chats/[chatId]/page.tsx, we can specify that this is a dynamic page by exporting a new value called dynamic set to "force-dynamic":

    export const dynamic = "force-dynamic";
    

    Upon saving and refreshing, any refreshing of the page will trigger a fresh component rendering process.

    Redirecting New Chats

    In situations where a new chat is initiated, it would be nice to navigate users to the specific chat page once they receive the first response.

    For this use case, the Next.js navigation router comes in handy.

    Inside of components/Chat.tsx, import the useRouter hook from Next.js:

    import { useRouter } from "next/navigation";
    

    Then inside of the Chat component, create a new variable router and assign it the value of useRouter:

    // inside of the `Chat` component above the onClick handler:
    
    let router = useRouter();
    

    With useRouter in scope, it is now possible to programmatically push users to a specified route.

    Inside of the onClick handler, after the call to getCompletion we'll check if there wasn't a chatId. If not, we'll push the user to the chat route with the id:

    const onClick = async () => {
      const completions = await getCompletion(chatId.current, [
        ...messages,
        {
          role: "user",
          content: message,
        },
      ]);
      if (!chatId.current) {
        router.push(`/chats/${completions.id}`);
        router.refresh();
      }
      chatId.current = completions.id;
      setMessage("");
      setMessages(completions.messages);
    };
    

    After saving the file, jump back to the browser and start a new chat.

    For example, enter the query "What is one plus five?" Once the response is received, you will be automatically redirected to the appropriate chat page– in this case chats/4.

    We know that navigating to a new chat works, but there are some safeguards we need to add.

    For example, what should happen when a user attempts to access a chat that doesn't exist? More importantly, what happens when a user tries to access a chat that's not theirs, via the id of that chat?

    Checking if a Chat Exists

    The initial check we'll add is to find out if a user is requesting a chat that doesn't exist. If the chat is not present in our database, we need to redirect the user accordingly.

    For this purpose, Next.js offers a notFound function from its Navigation library. This function is designed to handle 404 errors. Basically, when we don't get a chat from our database, we'll return notFound to signal to Next.js that this is an invalid route:

    // inside app/chats/[chatId]/page.tsx
    
    import { notFound } from "next/navigation";
    

    Inside the ChatDetail component, we'll check if the chat doesn't exist and return notFound if it doesn't:

    // inside the ChatDetail component
    
    const chat = await getChat(+chatId);
    if (!chat) {
      return notFound();
    }
    

    Verifying Chat Ownership

    The next step in our security measures is to verify if the requested chat actually belongs to the user making the request. To do this, we need to firstly understand who the user is.

    Since we're in a React server component, we'll use getServerSession from next-auth. We'll also add the redirect function from the Navigation library:

    // inside app/chats/[chatId]/page.tsx
    import { redirect, notFound } from "next/navigation";
    import { getServerSession } from "next-auth/react";
    

    After we've obtained the chat in the ChatDetail component, we can get the server session. If we don't have a session or the session's email doesn't match the user's email, then we'll redirect the user back to home:

    // inside of the ChatDetail component
    
    const session = await getServerSession();
    if (!session || session.user.email !== chat.user.email) {
      return redirect("/");
    }
    

    Let’s take our chat application one step further by refining the user experience.

    Imagine you want to access a specific chat, but you're not currently signed in. Your app should not just redirect you to the home, but rather initiate a sign-in process and then redirect you to your desired destination. This can be done with help from NextAuth middleware.

    NextAuth Middleware

    Inside the root of the src directory, create a new file called middleware.ts. This file will contain the middleware logic for NextAuth, which will intercept all incoming requests.

    In our case, we need to ensure that if a request is made for a specific chat, the user should be logged in.

    Inside the file, we'll export default from next-auth/middleware:

    // inside srt/middleware.ts
    
    export { default } from "next-auth/middleware";
    

    Then we add a config export that uses a matcher to see if the request is for a specific chat:

    export const config = { matcher: ["/chats/:chatid*"] };
    

    Save this file, then restart the dev server.

    Testing Route Redirection

    Back in the browser, visit the URL of a specific chat that doesn't exist. This time, instead of being redirected to the home page, you will be redirected to the sign-in page:

    The middleware is working

    After successfully signing in, you will be redirected back to the original chat you requested.

    This enhanced user experience is made possible by the callback URL provided by NextAuth after the sign-in process. The callback URL retains the intended destination within the application.

    With this feature working, now would be a good time to push an update to GitHub and kick off a new production deployment on Vercel.

    Next Steps

    In the next lesson, we'll go further into React server components and server-side data fetching in order to add a list of previous chats to the home page of the application.

    Transcript

    All right the next thing we want to do is experiment with parameterized routes. That would be something like for example /chats/id which will give you the chat for that particular id. So one thing we need to do is add on to our chat component some props because we're going to

    use the chat component in both the home page to start a chat but also this chat id route. So we're going to need to optionally take the chat id as well as the messages for that chat. So to do that we're going to add on some props. Some optional props first is going to be id that's going to be

    the id of our chat and then the messages. So we'll go and initialize our id down here with an incoming id and the messages with those initial messages. Now with that being done we can go and create our parameter route for chats. So we'll go back over in our app again do new file chats and then we

    use the brackets to give it a parameter. And a parameter I'm going to choose is chat id and then within that we're going to use page.tsx because this is going to be a react component. So there you go chats then within that brackets chat id and then within that page.tsx. All right so now we've

    got our chat detail component that's going to be returned from our page. So how do we get access to our params? How do we know what that chat id is? Well that actually comes in as a property. Check that out there's a special property called params and it's just got the list of params. So in

    this case it's got chat id as a string and if you have multiple parameterized sections this is just going to be additional keys on that params object. So now let's bring in our chat component because we're going to want to show that and then we're going to want to show that so I'm going to include that inside of a main. Now we do know the chat id so let's add that. Now we do know the id so let's

    add that but now we need the messages so let's use get chat from db to go and get the chat. And then since we're in a react server component all I need to do is simply just await get chat and now we've got our chat details. Awesome and now we can go and send

    those messages onto our component. All right looking pretty good let's give it a try. So to try it out I'm going to use chats id 3 as the chat. And there you go there was the chat that we started on the server we got the chat details and let's actually add some more.

    All right so we're getting an error okay what's going on oh okay so openai is getting some extra data on messages. So remember we're pulling messages from the database and that's got some

    extra fields on like chat id and id when what it really wants is just role and content. So it's being a little bit of a stickler it's basically saying hey don't send me any additional fields that I should just ignore but apparently we need to trim it down to just role and content. So let's

    go back over into our get completions and just be nice to openai and just give it just role and content. Let's see are you happy now openai? All right what is 70 times 80?

    All right good okay cool now let's make sure that it goes into the database. Okay and it's in the database cool awesome so now let me just do one thing let me hit refresh.

    All right so this is hitting a next.js route cache so next.js aggressively caches routes so unless you tell it any different it believes that if you requested chats 3 in this

    case in the past that it can go and cache that off and then just return that as the value if you ever ask for it again which is not the case here because we're updating things. So one way to fix this is to say that this is always going to be a dynamic page. There's another way using

    revalidate and we'll explore that in subsequent videos but for the moment I just want to tell next.js that this is a dynamic page and you should always go back to the database and get new content for this page. So let's go back over into our page and pretty much anywhere in here we're going to

    expose a new value called dynamic we're going to say that this is a dynamic route so we're going to force dynamic so let's hit save and refresh and you can see now any time that we request this page it is going to go and re-render the component entirely. All right now there's a couple more

    things that I want to do first over here when we start a chat I want to go and once we get our first response back I want to forward you on to the page for that chat so I'm going to use the router for that. So let's go back into our chat and from next navigation I'm going to bring in

    the use router hook and the use router hook allows us to push programmatically the user to a route. So what I'm going to do is down here if we know now that we got an id back and we were at null then we're going to push you to /chats with that id. So to do that we first invoke use router

    and we get back our router and then down here we say well if we didn't have a chat id at the start which means that we were null but we do now have one we're going to push you to that chat's route

    so let's hit save and now over here on the home page if I say what is one plus five then we'll get the response back and we'll go to the right route so in this case chats four and look get

    when AI is being really terse today six usually says like one plus five equals six I don't know AI. Okay so now this chat's route is a little bit open one I can request a chat that doesn't exist and I should go to like a 404 for that and two what if I go and give it an id of a chat that's

    not mine I want to go and actually just send you back to home like that's not okay you shouldn't know what that id is so let's go and implement those safeguards on our chat route. So the first check I'm going to make is are you requesting a chat that doesn't exist so if I find that that

    chat doesn't exist I need to do something with that so there is a not found function you can bring in from Next Navigation you basically say hey this is a 404. So I'm going to say that if we don't get back a chat which means that there's nothing in database for that then we just return

    this not found that's a way to signal to Next.js that this is not a valid route. Now the next thing I want to do is make sure that is the chat actually yours so first we need to know who you are let's bring in get server session because it's a react server component therefore we use get server session and if you're not the right person then we're going to redirect so we'll bring in

    redirect. And then down here after we have the chat we'll get that server session and we'll say if we don't have a session or the emails don't match then we'll just send you back home. All right let's hit save. All right now there's one more way that we can secure this application and

    actually make it a little bit nicer in the process. So let's say we got chats for here now I'm going to sign out and I'm going to try again chats for and it's going to do the thing where it redirects me to home because we don't have a session but

    what we really want is to initiate a sign-in and then if you successfully sign in then to go to that deep route. So the way that we're going to do that is we're going to add some middleware for NextAuth. So in Next.js you can create a file called middleware.ts. That's generic middleware

    for Next.js. You can use it to intercept any incoming requests and do whatever you want on them. In this case we're just going to use NextAuth to basically say if you get a request for slash chats slash and then a chat id then just make sure that you are logged in. And let's see let's hit

    save and I'll reboot the server. Let's try this again. So I'm going to go to slash chat slash four and now instead of just being redirected to home I am instead redirected to sign in and we get this

    cool callback url which is going to be used once we actually sign in. So I'll sign in with github and now redirected deeply to that same link. Nice a really good experience for a user. All right I think we're good let's go and upload this to production. So again I'll stop the server

    I'll commit this as param routes and let's go take a look at versal. All right let's go take a look at it in production. Nice we're logged in.

    We'll go to that fourth chat page. Awesome let's try out a new chat. And there we go awesome now actually redirected to that

    chat route and it looks good. In the next section we're going to delve a little bit deeper into react server components and server side data fetching by putting a list of previous chats on this home page. I'll see you in the next session.