DEV Community

Cover image for How To Manage Access and Refresh Tokens for an API in Next.js 13 With Supabase and Prisma
andrew shearer
andrew shearer

Posted on • Updated on

How To Manage Access and Refresh Tokens for an API in Next.js 13 With Supabase and Prisma

This article will show you how to use the Trackmania API in a Next.js 13 app using TypeScript. The API uses access and refresh tokens, so it is not as easily accessible as other APIs, like jsonplaceholder , or an API where you can include the same key(s) with every request.

So, we will use Supabase as a Postgres database to store the tokens and Prisma as our ORM of choice to communicate with our database.

The main focus of this article is to show you how to store / retrieve these tokens for API use, NOT for user authentication. If you’re looking for user authentication, I’d recommend looking at an example here over on NextAuth’s website to get you started.

Let’s get started!

Initial Setup

Quickly download and setup the latest Next.js TypeScript starter:

npx create-next-app@latest --ts .
Enter fullscreen mode Exit fullscreen mode

If you are getting warnings in your CSS file complaining about unknown CSS rules, follow these steps here

Still in globals.css, update the code with this reset from Josh Comeau

/* src/app/globals.css */

@tailwind base;
@tailwind components;
@tailwind utilities;

*, *::before, *::after {
  box-sizing: border-box;
}

* {
  margin: 0;
    padding: 0;
}

body {
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}

img, picture, video, canvas, svg {
  display: block;
  max-width: 100%;
}

input, button, textarea, select {
  font: inherit;
}

p, h1, h2, h3, h4, h5, h6 {
  overflow-wrap: break-word;
}

#root, #__next {
  isolation: isolate;
}
Enter fullscreen mode Exit fullscreen mode

Update tsconfig.json to this:

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "Node",
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}
Enter fullscreen mode Exit fullscreen mode

Update src/app/page.tsx to this:

// src/app/page.tsx

const HomePage = () => {
  return (
    <div>
      <h1>HomePage</h1>
    </div>
  );
};

export default HomePage;
Enter fullscreen mode Exit fullscreen mode

Update src/app/layout.tsx to this:

// src/app/layout.tsx

import { Inter } from "next/font/google";

import type { Metadata } from "next";
import type { ReactNode } from "react";

import "./globals.css";

const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  description: "Generated by create next app",
  title: "Trackmania API Demo"
};

type RootLayoutProps = {
  children: ReactNode;
};

const RootLayout = ({ children }: RootLayoutProps) => {
  return (
    <html lang="en">
      <body className={inter.className}>{children}</body>
    </html>
  );
};

export default RootLayout;
Enter fullscreen mode Exit fullscreen mode

GitHub & Vercel

I’m a fan of setting up a GitHub repo and deploying to Vercel early, and committing + pushing often in smaller chunks

This way, if the site should not build on Vercel, it’s a lot easier to figure when the issue started occurring.

I’m going to assume that since you’re doing this advanced tutorial that you:

  1. Already know how to create a repo on GitHub and push your code up to it
  2. Deploy a website from your Vercel dashboard by selecting the repo you’ve created

Please do both of these things before continuing.

Getting API Access Token

In order to use Nadeo’s Trackmania API, we need to get an access_token (which also comes with a refresh_token)

There are 2 approaches to get this:

  1. Use a dedicated Ubisoft account
  2. Create Server Account on trackmania.com

While it is true that to login to trackmania.com, you need a Ubisoft account anyways, the flow for refreshing the access_token is much simpler using a Server Account.

On trackmania.com, once you are signed in, click on your user name in the top right corner

In the dropdown, select My Server Accounts

Enter a value for the Server Login (you can think of this is a project name)

I’ll call mine: trackmania-api-demo

Click Submit

Once you do, a message will appear to Please copy the password

Copy the Login (the name you just entered, ie trackmania-api-demo) and password into a text file for now

Let’s use Postman (or a similar platform of your choice) to send requests first to make sure things will work before coding it all up.

Create a new request with the following:

  • Request type to POST
  • URL to https://prod.trackmania.core.nadeo.online/v2/authentication/token/basic
  • Under Authorization:
    • Set the Type to Basic Auth
    • For the Username and Password, copy / paste the Login and Password fields from the Server Account
  • Under Headers, add
    • Content-Type: application/json
  • Under Body

    • Set the type to raw, then select JSON
    • Add this:
    {
      "audience": "NadeoLiveServices"
    }
    
    
    • NOTE: We have to include this JSON object in the body because we need a way of telling the API what to authenticate with. You can see a mapping of APIs and available audiences here.

Click Send, and you should see a response like this:

{
  "accessToken":"eyJhbGciOiJIUzI1NiIsImVudiI6InRyYWNrbWFuaWEtcHJvZCIsInZlciI6IjEifQ.PAYLOAD.SIGNATURE",
  "refreshToken":"eyJhbGciOiJIUzI1NiIsImVudiI6InRyYWNrbWFuaWEtcHJvZCIsInZlciI6IjEifQ.PAYLOAD.SIGNATURE"
}
Enter fullscreen mode Exit fullscreen mode

The start of the access_tokens, known as the HEADER, is always the same, so it’s safe to display them publicly like this. However, it’s the PAYLOAD and SIGNATURE parts of the tokens that are unique.

If you URL-base64-decode the PAYLOAD part of the accessToken, you get the following JSON object:

{
  "jti": "UUID_HERE",
  "iss": "NadeoServices",
  "iat": 1595191016,
  "rat": 1595192816,
  "exp": 1595194616,
  "aud": "NadeoLiveServices",
  "usg": "Server",
  "sid": "UUID_HERE",
  "sub": "UUID_HERE",
  "aun": "mm",
  "rtk": false,
  "pce": false
}
Enter fullscreen mode Exit fullscreen mode

Here, exp defines the expiration time, and rat defines the time after which you are able to refresh the token.

To URL-base64-decode something in TypeScript, you can use a function like this:

const urlBase64Decode = (input: string) => {
  // decode the base64 string
  const base64Decoded = atob(input);

  // URL-decode the data
  const urlDecoded = decodeURIComponent(base64Decoded);

  // parse the JSON data
  return JSON.parse(urlDecoded);
};

Enter fullscreen mode Exit fullscreen mode

Using the Access Token

To keep things simple, let’s try and fetch the data for a single map for now to make sure our access_token is working

Let’s fetch data for the first map in the current campaign (which at the time of writing, is Summer 2023-01)

We will need send a GET request to the following URL:

<https://live-services.trackmania.nadeo.live/api/token/map/MAP_UID>

Enter fullscreen mode Exit fullscreen mode

To get a MAP_UID, head back to the official Trackmania site: https://www.trackmania.com/

Once there, click on Campaigns

Then click SUMMER 2023 - 01 on the next page

The URL will be this: https://www.trackmania.com/tracks/7hk8IflYsbMbpJv2gyYzx48Zvt7

The last part in the path, 7hk8IflYsbMbpJv2gyYzx48Zvt7, is the MAP_UID

Back in Postman, create another request tab and set the following:

Click Send, and you should see a response like this:

{
    "uid": "7hk8IflYsbMbpJv2gyYzx48Zvt7",
    "mapId": "a93f902b-16a1-4448-ad34-65cf32d41425",
    "name": "Summer 2023 - 01",
    "author": "d2372a08-a8a1-46cb-97fb-23a161d85ad0",
    "submitter": "d2372a08-a8a1-46cb-97fb-23a161d85ad0",
    "authorTime": 23997,
    "goldTime": 26000,
    "silverTime": 29000,
    "bronzeTime": 36000,
    "nbLaps": 0,
    "valid": false,
    "downloadUrl": "https://prod.trackmania.core.nadeo.online/storageObjects/5e1cd15d-0b55-4db7-98ad-a82656ceb112",
    "thumbnailUrl": "https://prod.trackmania.core.nadeo.online/storageObjects/fa836805-f906-41c1-9ca2-e52a63c4b44f.jpg",
    "uploadTimestamp": 1687274471,
    "updateTimestamp": 1687274472,
    "fileSize": null,
    "public": false,
    "favorite": false,
    "playable": true,
    "mapStyle": "",
    "mapType": "TrackMania\\TM_Race",
    "collectionName": "Stadium",
    "gamepadEditor": false
}
Enter fullscreen mode Exit fullscreen mode

Hazah! If you’ve made it this far, congrats! This is a very good milestone to hit.

Planning Out the App

Ok, so now that we’ve tested some queries in Postman to ensure we get the responses we want, we should pause here and plan out how we want to do code wise:

  • Store the access_ and refresh_token in Supabase database
  • Check whether or not the current access_token is expired
  • If it is, make a request with the refresh_token to get a new one
  • Store the new access_token in the Supabase database

Most resources online I’ve seen suggest using a JWT to store the refresh_token. However, using a JWT to store the refresh_token is less secure than saving it in a database.

Instead, we will use Supabase to store our tokens, and use Prisma as our ORM of choice to read and write data to / from the database.

Install and Setup Prisma

First, let’s install Prisma as a devDependency

npm i -D prisma
Enter fullscreen mode Exit fullscreen mode

Next, we can initialize Prisma by running:

npx prisma init
Enter fullscreen mode Exit fullscreen mode

This will create an .env file as well as a prisma folder, both at the root level

NOTES:

  • Do NOT change .env file name to .env.local, it must stay as .env
  • The DATABASE_URL will need to be updated with the URL you get when creating a DB with Supabase later on

Setup PostgreSQL on Supabase

Create an account if you do not have one already

Once logged in, go to your Dashboard

On the dashboard page, click New project

Give it a name, something in kebal-case-like-this

Click generate a password

Copy and paste the generated password into the .env file as a comment for now

Change your region accordingly

Click Create new project

Navigate to the project settings (gear icon, very bottom of sidebar nav)

Click on Database

Under Connection string, click URI

Copy this value

Back in the .env file, replace the default DATABASE_URL with the URI you just copied.

Also update the [YOUR-PASSWORD] placeholder with the generated password from before.

Back in Supabase, click on the Database icon.

Under tables, there should be nothing there. But not for long!

Create Prisma Model Schema

Open the schema.prisma file generated earlier

We’ll add a simple ApiToken model:

model ApiToken {
  tokenId        String   @id @default(cuid())
  accessToken    String
  expirationDate DateTime
  refreshToken   String
}
Enter fullscreen mode Exit fullscreen mode

This will be sufficient because we are not performing any auth, we are simply saving the keys in a secure location for when we want to reach out to the Trackmania API

While we’re at it, let’s also update the .env file with Trackmania Server Account login and password from earlier:

DATABASE_URL="..."

TM_SERVER_ACCOUNT_LOGIN="..."
TM_SERVER_ACCOUNT_PASSWORD="..."
Enter fullscreen mode Exit fullscreen mode

We’ll need these whenever we make the initial POST request to get the tokens

Now we need to migrate

  • NOTE: also any time the schema is changed, you’ll have to migrate

Run this command:

npx prisma migrate dev --name init
# "init" at the end is the name of the migration
Enter fullscreen mode Exit fullscreen mode

Go back to Supabase, and click on the Table Editor icon

You should now see a User table!

Install Prisma Client

Link to Prisma Client docs here

Install it:

npm install @prisma/client
Enter fullscreen mode Exit fullscreen mode

Next, inside the src folder, create a folder called lib

In the lib folder, create a file called prisma.ts

There are some common / well known issues when working with Next.js and Prisma

There is a docs page about it here

Copy and paste this snippet (taken from the docs) into prisma.ts:

import { PrismaClient } from '@prisma/client'

const prismaClientSingleton = () => {
  return new PrismaClient()
}

type PrismaClientSingleton = ReturnType<typeof prismaClientSingleton>

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClientSingleton | undefined
}

const prisma = globalForPrisma.prisma ?? prismaClientSingleton()

export default prisma

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
Enter fullscreen mode Exit fullscreen mode

What this code does is when we are not in the production environment, and the Prisma client has already run, the PrismaClient won’t be initialized again and again every time we restart the server.

At this point, it’s a good idea to update our Environment Variables on Vercel with the values we’ve added.

From your dashboard, select the project you’ve created / deployed.

Click on Settings, then Environment Variables.

Add them here (you can actually copy / paste multiple at a time!).

Adding a Row in Supabase

Head over to Supabase, and enter in a row manually.

Perform a POST request in Postman to get an up-to-date access and refresh token

You can then use the urlBase64Decode function shown earlier to get the exp value

You can then use this function to create a Date string from that value:

const convertUnixTimestampToDate = (timestamp) => new Date(timestamp * 1000);

// example
convertUnixTimestampToDate(1695344641)
"Thu Sep 21 2023 18:04:01 GMT-0700 (Pacific Daylight Time)"
Enter fullscreen mode Exit fullscreen mode

Use this value for the expirationDate

For the tokenId, you can also use this generateCUID function in a browser console to generate an id for you (thanks ChatGPT!):

function generateCUID() {
  // Define a list of characters that can be used in the CUID
  const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

  // Length of the CUID
  const length = 25;

  let cuid = 'c'; // Start with the letter 'c' to mark it as a CUID

  for (let i = 0; i < length; i++) {
    const randomIndex = Math.floor(Math.random() * characters.length);
    cuid += characters[randomIndex];
  }

  return cuid;
}

console.log(generateCUID());
// cVuBo6oMxjNbc7WKpMrT6lqQg3
Enter fullscreen mode Exit fullscreen mode

Using Next.js middleware

Since we need to always be checking whether or not the access_token is valid or not, we need some code to run on every page

A simple way to implement this is by using middleware

Inside the src directory, create a file called middleware.ts

Add this code to it for now:

// src/middleware.ts

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

// This function can be marked `async` if using `await` inside
const middleware = (request: NextRequest) => {
  console.log("middleware is running!");

  // return NextResponse.json(request)
};

export { middleware };
Enter fullscreen mode Exit fullscreen mode

Then if you run npm run dev, you should see the console.log when you visit / refresh the home page.

Next, let’s update this code to fetch the example row from Supabase we added earlier:

import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
import prisma from "./lib/prisma";

// This function can be marked `async` if using `await` inside
const middleware = async (request: NextRequest) => {
  // console.log("middleware running!!!");

  // return NextResponse.json(request)
  const apiTokenTest = await prisma.apiToken.findUnique({
    where: {
      tokenId: "c1Z1oSjpTT3REWxoeuK0xcWQBj"
    }
  });

  console.log("apiTokenTest: ", apiTokenTest);
};

export { middleware };
Enter fullscreen mode Exit fullscreen mode

However, when we do this, we will get this error:

At the time of writing, as the error says, PrismaClient is unable to run in Vercel Edge Functions.

Thankfully, it is being discussed and it looks like a solution is being worked on.

To get around this, we can use Prisma DataProxy.

To start, head over to https://cloud.prisma.io/ and create an account with your GitHub login.

Once you’ve authorized Prisma, go to your projects dashboard here (you also might automatically be directed to the create a project flow).

Click on Create classic project.

Copy and paste your DATABASE_URL from the .env file into the Connection string field.

Click Create project.

Add your GitHub profile if not selectable.

Select your repo, and make sure the branch is set to main.

Create a Data Proxy connection string, give it a name.

Copy the value, and head back to the code and open the .env file.

Rename the existing DATABASE_URL variable to MIGRATE_DATABASE_URL.

Then create a DATABASE_URL, this time setting it to the proxy connection string you just created.

The MIGRATE_DATABASE_URL will be used to apply and pending migration to the database as the app will be deployed on Vercel.

Next, open up the terminal and run:

npx prisma generate --data-proxy
Enter fullscreen mode Exit fullscreen mode

This will generate a Prisma client that will use Prisma data proxy

Next, go to prisma.ts and adjust the import path of the PrismaClientlike so:

// before
import { PrismaClient } from "@prisma/client";

// after
import { PrismaClient } from "@prisma/client/edge";
Enter fullscreen mode Exit fullscreen mode

After that, run npm run dev in the terminal, and you should now see the response:

apiTokenTest:  {
  tokenId: 'TOKEN_ID_HERE',
  accessToken: 'ACCESS_TOKEN_HERE',
  expirationDate: 2023-09-21T18:04:01.000Z,
  refreshToken: 'REFRESH_TOKEN_HERE'
}

Enter fullscreen mode Exit fullscreen mode

Updating the Prisma Schema

We should update the ApiToken model to also have a addedOn field

This will help us get the row in the database that was added most recently

model ApiToken {
  tokenId        String   @id @default(cuid())
  accessToken    String
  expirationDate DateTime
  refreshToken   String
  addedOn        DateTime @default(now())
}
Enter fullscreen mode Exit fullscreen mode

We will also need to add a directUrl property to the datasource db:

datasource db {
  provider  = "postgresql"
  url       = env("DATABASE_URL")
  directUrl = env("MIGRATE_DATABASE_URL") // <---
}
Enter fullscreen mode Exit fullscreen mode

Again, the MIGRATE_DATABASE_URL is the old DATABASE_URL that was copied from Supabase.

Make sure to also add / update your Environment Variables on Vercel too!

Now we can run migrate command:

npx prisma migrate dev --name added_on_field
Enter fullscreen mode Exit fullscreen mode

If you didn’t specify the directUrl, you would get an error. So that is why we added it.

Link to the docs page about using Prisma Migrate here

Finally, we will need to add this script to our package.json (as per Prisma docs here):

"postinstall": "prisma generate"
Enter fullscreen mode Exit fullscreen mode

We do this because you would run into the following error message on deployment otherwise:

Prisma has detected that this project was built on Vercel, which caches dependencies.
This leads to an outdated Prisma Client because Prisma's auto-generation isn't triggered.
To fix this, make sure to run the `prisma generate` command during the build process.

Learn how: <https://pris.ly/d/vercel-build>
Enter fullscreen mode Exit fullscreen mode

Updating the middleware

So now we can begin writing the necessary code to check our tokens in the database

But first, let’s setup a simple request on the home page knowing it will fail:

const getMapInfo = async () => {
  const mapUId = "7hk8IflYsbMbpJv2gyYzx48Zvt7";
  const url = `https://live-services.trackmania.nadeo.live/api/token/map/${mapUId}`;

  const res = await fetch(url, {
    method: "GET",
    headers: {
      "Content-Type": "application/json"
    }
  });

  if (!res.ok) {
    const text = await res.text(); // get the response body for more information

    throw new Error(`
      Failed to fetch data
      Status: ${res.status}
      Response: ${text}
    `);
  }

  return res.json();
};

const HomePage = async () => {
  const mapInfo = await getMapInfo();

  console.log("mapInfo: ", mapInfo);

  return (
    <div>
      <h1>HomePage</h1>

      <p>Track name:</p>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

As you can see, we are missing the Authorization along with the accessToken. This is where middleware can help us.

We can use it to set headers on requests. We will use it to set the access_token on each request to the Trackmania API. In the end, it looks like this:

// src/middleware.ts

import { NextRequest, NextResponse } from "next/server";
import prisma from "./lib/prisma";
import { unixTimestampToDate, urlBase64Decode } from "./lib/utils";

import type { AuthTokenResp } from "./types";

const middleware = async (request: NextRequest) => {
  try {
    const mostRecentApiToken = await prisma.apiToken.findFirst({
      orderBy: {
        addedOn: "desc"
      }
    });

    if (mostRecentApiToken) {
      const { accessToken, expirationDate, refreshToken } = mostRecentApiToken;

      const tokenExpirationDate = new Date(expirationDate);
      const currentDate = new Date();

      // compare the current date to the expiration date
      if (currentDate > tokenExpirationDate) {
        console.log("the token is expired - get a new one");

        const url = "https://prod.trackmania.core.nadeo.online/v2/authentication/token/basic";
                const login = process.env.TM_SERVER_ACCOUNT_LOGIN;
        const password = process.env.TM_SERVER_ACCOUNT_PASSWORD;

        const res = await fetch(url, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            Authorization: `Basic ${btoa(`${login}:${password}`)}`
          },
          body: JSON.stringify({
            audience: "NadeoLiveServices"
          })
        });

        if (!res.ok) {
          const text = await res.text(); // get the response body for more information

          throw new Error(`
            Failed to fetch data
            Status: ${res.status}
            Response: ${text}
          `);
        }

        const newTokens: AuthTokenResp = await res.json();
        const { accessToken: newAccessToken, refreshToken: newRefreshToken } = newTokens;

        const [header, payload, structure] = newAccessToken.split(".");
        const { exp, rat } = urlBase64Decode(payload);
        const newExpirationDate = unixTimestampToDate(rat);

        await prisma.apiToken.create({
          data: {
            accessToken: newAccessToken,
            expirationDate: newExpirationDate,
            refreshToken: newRefreshToken,
            addedOn: new Date()
          }
        });
      }

      console.log("the token is valid - you good!");

            // set a new response header "Authorization"
      const response = NextResponse.next();
      response.headers.set("Authorization", `nadeo_v1 t=${accessToken}`);
      return response;
    }
  } catch (error) {
    return NextResponse.json(
      {
        message: "Something went wrong!"
      },
      {
        status: 500
      }
    );
  }
};

export { middleware };
Enter fullscreen mode Exit fullscreen mode

That’s quite a lot! Let’s break it down:

  • First, we the most recent row in the database by using the addedOn field
  • Then, we check we have something we can work with
  • If we do, destructure the accessToken, expirationDate, and refreshToken variables out of mostRecentApiToken
  • We then check if the currentDate is greater than tokenExpirationDate
  • If it is, that means the access_token is expired and we need to get a new one

    • We then make the necessary POST request, using the refresh token for the Authorization header
    • We then add on a type annotation of AuthTokenResp, seen below, to the newTokens:
    // src/types/index.ts
    
    type AuthTokenResp = {
      accessToken: string;
      refreshToken: string;
    };
    
    • When the destructure again, this time using aliases to avoid naming conflicts
    • We then split the access_token into 3 parts, mostly just for the payload
    • We then run the payload through our urlBase64Decode function, which itself now has an updated return type DecodedPayload. That type looks like this:
    // src/types/index.ts
    
    type DecodedPayload = {
      aud: string;
      aun: string;
      exp: number;
      iat: number;
      iss: string;
      jti: string;
      pce: boolean;
      rat: number;
      rtk: boolean;
      sid: string;
      sub: string;
      usg: string;
    };
    
    • We can then destructure yet again, this time getting the value for rat
    • We then run the rat value through a unixTimestampToDate conversion function, which looks like this:
    const unixTimestampToDate = (unixTimestamp: number): Date => {
      return new Date(unixTimestamp * 1000);
    };
    
    • Finally, after all that, we can properly setup the create call to add the new tokens to the database
  • And outside this expiration check, we set new a Authorization header using the most recent access token

Whew!

Now we can go back to the home page and adjust our query:

// src/app/page.tsx

import { headers } from "next/headers";
import type { TrackmaniaTrack } from "@/types";

const getMapInfo = async (): Promise<TrackmaniaTrack> => {
  const mapUId = "7hk8IflYsbMbpJv2gyYzx48Zvt7";
  const url = `https://live-services.trackmania.nadeo.live/api/token/map/${mapUId}`;

  const auth = headers().get("Authorization") || "";

  const res = await fetch(url, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
      Authorization: auth
    }
  });

  if (!res.ok) {
    const text = await res.text(); // get the response body for more information

    throw new Error(`
      Failed to fetch data
      Status: ${res.status}
      Response: ${text}
    `);
  }

  return res.json();
};

const HomePage = async () => {
  const mapInfo = await getMapInfo();

  console.log("mapInfo: ", mapInfo);

  return (
    <div>
      <h1>HomePage</h1>

      <p>Track name: {mapInfo.name}</p>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Here, we leverage the added on headers using the headers function from next/headers. It is read only, but that is perfectly fine since we just want to include it with our request anyways. If you don’t include this, the request fails.

Here is what the TrackmaniaTrack type looks like:

type TrackmaniaTrack = {
  author: string;
  authorTime: number;
  bronzeTime: number;
  collectionName: string;
  downloadUrl: string;
  favorite: boolean;
  fileSize: null;
  gamepadEditor: boolean;
  goldTime: number;
  mapId: string;
  mapStyle: string;
  mapType: string;
  name: string;
  nbLaps: number;
  playable: boolean;
  public: boolean;
  silverTime: number;
  submitter: string;
  thumbnailUrl: string;
  uid: string;
  updateTimestamp: number;
  uploadTimestamp: number;
  valid: boolean;
};
Enter fullscreen mode Exit fullscreen mode

And now, if you check the terminal, you should get the correct response, as well as the map name should appear on the screen!

Minor Adjustments

One thing I noticed was that sometimes the site errors out instead of using the most recent access_token. The solution is to simply refresh the page. So, as a temporary fix, I’ve created some basic back up pages:

// src/app/error.tsx

"use client";

import Link from "next/link";
import { useRouter } from "next/navigation";

const RootErrorPage = () => {
  const router = useRouter();

  return (
    <div>
      <h1 className="text-4xl">Oops!</h1>
      <p>Something went wrong there. You can try refreshing the page, or go back home.</p>

      <button
        className="bg-teal-600 text-white mr-6 py-2 px-4"
        onClick={() => router.refresh()}
        type="button"
      >
        Refresh
      </button>

      <Link className="border-teal-600 border-2 py-2 px-4" href="/">
        Go Home
      </Link>
    </div>
  );
};

export default RootErrorPage;
Enter fullscreen mode Exit fullscreen mode
// src/app/not-found.tsx

import Link from "next/link";

const RootNotFound = () => {
  return (
    <div>
      <h1 className="text-4xl">Oops!</h1>
      <p>Looks like you tried accessing a page that doesn't exist.</p>
      <Link className="border-teal-600 border-2 inline-block py-2 px-4" href="/">
        Go Home
      </Link>
    </div>
  );
};

export default RootNotFound;
Enter fullscreen mode Exit fullscreen mode

I also created a root level loading.tsx so something is shown while the site is loading, which provides a better UX:

// src/app/loading.tsx

const RootLoading = () => {
  return <p>Loading...</p>;
};

export default RootLoading;
Enter fullscreen mode Exit fullscreen mode

Outro

I cannot stress how helpful the documentation over on Openplanet was in building this app. Big thanks to them! You can also view a list of the most common endpoints here.

And that’s it! As stated in the beginning, this was mainly to demonstrate how to use Supabase to store and retrieve tokens in a secure fashion instead of using JWT for API use. Most resources I found were revolving around authorization / user accounts, not using just an API. And I wanted to push myself and reinforce what I’ve been learning lately, and I’m very proud of myself for figuring this out.

The next steps are, well, up to you! If you want to continue building out your own Trackmania web app, you now have the proper foundation to do so. If not, you have the knowledge for storing tokens if the API you are using has a similar setup.

You could perhaps display maps from the current season on the home page, and create dynamic routes to each of them. Then, on an individual map page, display some information about that map.

The next major feature would be to include NextAuth using Ubisoft as a custom provider. Buuuttt I’m gonna leave that for another day ;)

I hope you found this article helpful. As always, you can view the full source here in case you get stuck.

If you have any feedback for me on how I could improve the code, please let me know! This was only the second time I’ve built a project using this stack, so any suggestions you may have are greatly appreciated!

Cheers, and happy coding!

Top comments (0)