Skip to content
On this page

Developer Blog

Build a URL Shortener with React, TypeScript, and Directus

Published June 27th, 2024

Written By
Onyedikachi Eni
Onyedikachi Eni
Guest Author

In this tutorial, we will build a link shortener. Then, we'll create a React project that looks for a slug, queries the associated record in Directus, and redirects the user to the configured link.

Before You Start

You will need:

  1. A Directus project - follow our quickstart guide if you don't already have one.
  2. Some knowledge of Javascript and React.js.

The complete code for this project can be found on GitHub.

Setting Up Your Directus Project

Create a new collection called short_link. Enable all optional fields, and add the following additional fields:

  • slug (type: String, interface: input, required): The URL path that will be shared in the short URL.
  • url (type: String, interface: input, required): The URL we want to redirect to.
  • clicks (type: Integer, interface: input, default_value: 0): Number of times the link was clicked.

To make sure the URL that will be stored in the URL field is valid, we will use a validation filter.

Create a validation rule for the URL. From the field settings, ensure the URL Matches RegExp:

https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)
https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)

Validation regex

Create a new contributor role will have the following privileges:

  • Create a short_link.
  • Read a short_link
  • Update the clicks field (to avoid spamming) of short_link.

Contributor Permissions - create and read short link are enabled. Edit is custom.

Contributor Privileges

Finally, create some sample data in your short_link collection.

SlugURL
websitehttps://directus.io/
xhttps://x.com/directus

Setting Up Your React Project

Install React.js, set up dependencies, and run a development server:

npm create vite@latest link-shortener
? Select a framework: React
? Select a variant: TypeScript
npm install
npm install --save-dev @types/node
npm install react-router-dom
npm install @directus/sdk 
npm run dev
npm create vite@latest link-shortener
? Select a framework: React
? Select a variant: TypeScript
npm install
npm install --save-dev @types/node
npm install react-router-dom
npm install @directus/sdk 
npm run dev

Now, open http://localhost:5173/ on your browser, and you should see the starter page.

Next, we will set up Vite to allow environment variables, which will be used to store our Directus credentials. In the vite.config.ts file, add the following code:

typescript
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  define: {
    "process.env.VITE_DIRECTUS_API_TOKEN.": JSON.stringify(
      process.env.VITE_DIRECTUS_API_TOKEN
    ),
    "process.env.VITE_DIRECTUS_API_URL.": JSON.stringify(
      process.env.VITE_DIRECTUS_API_URL
    ),
  },
});
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig({
  plugins: [react()],
  define: {
    "process.env.VITE_DIRECTUS_API_TOKEN.": JSON.stringify(
      process.env.VITE_DIRECTUS_API_TOKEN
    ),
    "process.env.VITE_DIRECTUS_API_URL.": JSON.stringify(
      process.env.VITE_DIRECTUS_API_URL
    ),
  },
});

Create a .env file in the src directory, and the following variables, being sure to provide your specific static authentication token and project URL:

VITE_DIRECTUS_API_TOKEN = XXXXX
VITE__DIRECTUS_API_URL = https://your-amazing.directus.app/
VITE_DIRECTUS_API_TOKEN = XXXXX
VITE__DIRECTUS_API_URL = https://your-amazing.directus.app/

In the src directory create a sub-directory called utils, and within it, a directus.ts file.

Add the following code to directus.ts:

typescript
import { createDirectus, staticToken, rest } from "@directus/sdk";

const directusToken = import.meta.env.VITE_DIRECTUS_API_TOKEN;
const directusUrl = import.meta.env.VITE__DIRECTUS_API_URL;

if (!directusToken) {
  throw new Error("Please include a Token");
}

if (!directusUrl) {
  throw new Error("Please include a Url");
}

export const directus = createDirectus(directusUrl)
  .with(staticToken(directusToken))
  .with(rest());
import { createDirectus, staticToken, rest } from "@directus/sdk";

const directusToken = import.meta.env.VITE_DIRECTUS_API_TOKEN;
const directusUrl = import.meta.env.VITE__DIRECTUS_API_URL;

if (!directusToken) {
  throw new Error("Please include a Token");
}

if (!directusUrl) {
  throw new Error("Please include a Url");
}

export const directus = createDirectus(directusUrl)
  .with(staticToken(directusToken))
  .with(rest());

Creating the Dynamic Route

Let's create the route that will be used to redirect the user to the slug URL queried from Directus. In the App.tsx file add the following code:

typescript
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { useParams } from "react-router-dom";

function App() {
  return (
    <>
      <h1>Link Shortener</h1>
      <BrowserRouter>
        <Routes>
          <Route path="/:slug" element={<LinkRoute />}></Route>
        </Routes>
      </BrowserRouter>
    </>
  );
}

function LinkRoute() {
  const { slug } = useParams();

  console.log(slug);

  return (
    <>
      <h1>{slug}</h1>
    </>
  );
}

export default App;
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { useParams } from "react-router-dom";

function App() {
  return (
    <>
      <h1>Link Shortener</h1>
      <BrowserRouter>
        <Routes>
          <Route path="/:slug" element={<LinkRoute />}></Route>
        </Routes>
      </BrowserRouter>
    </>
  );
}

function LinkRoute() {
  const { slug } = useParams();

  console.log(slug);

  return (
    <>
      <h1>{slug}</h1>
    </>
  );
}

export default App;

Querying Directus

Query the associated record for the slug in Directus and redirect the user to the route link.

Enter the following code in App.tsx to define the interface for the data that will be returned from Directus:

typescript
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { useParams } from "react-router-dom";

interface ShortLink { 
  clicks: number; 
  date_created: string; 
  date_updated?: string; 
  id: number; 
  slug: string; 
  sort?: null; 
  url: string; 
  user_created?: string; 
  user_updated?: null; 
} 

function App() {
  return (
    <>
      <h1>Link Shortener</h1>
      <BrowserRouter>
        <Routes>
          <Route path="/:slug" element={<LinkRoute />}></Route>
        </Routes>
      </BrowserRouter>
    </>
  );
}

function LinkRoute() {....}
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { useParams } from "react-router-dom";

interface ShortLink { 
  clicks: number; 
  date_created: string; 
  date_updated?: string; 
  id: number; 
  slug: string; 
  sort?: null; 
  url: string; 
  user_created?: string; 
  user_updated?: null; 
} 

function App() {
  return (
    <>
      <h1>Link Shortener</h1>
      <BrowserRouter>
        <Routes>
          <Route path="/:slug" element={<LinkRoute />}></Route>
        </Routes>
      </BrowserRouter>
    </>
  );
}

function LinkRoute() {....}

Query Directus in App.tsx:

typescript
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { useParams } from "react-router-dom";
import { directus } from "./util/directus"; 
import { readItems, updateItem } from "@directus/sdk"; 
import { useEffect, useState } from "react"; 

function App(){...}

function LinkRoute() { 
  const { slug } = useParams();
  const [slugError, setSlugError] = useState(""); 

  useEffect(() => { 
    async function fetchShortLink() { 
      try { 
        const data = await directus.request(readItems("short_link")); 

        const slug_data = data 
          .map((y) => y) 
          .filter((z) => z.slug.toLowerCase().includes(slug)); 

        if (!slug_data || slug_data?.length === 0) { 
          setSlugError(`Invalid Slug: Couldn't find the record →→ ${slug}`); 
          throw new Error("Invalid Slug: Couldn't find that record"); 
        } 
        const shortLink = slug_data[0] as ShortLink; 

        await directus.request( 
          updateItem("short_link", shortLink.id, { 
            clicks: shortLink.clicks + 1, 
          }) 
        ); 

        window.location.assign(`${shortLink.url}`);

      } catch (error) { 
        console.log(error); 
      } 
    } 

    fetchShortLink(); 
  }, [slug]); 

  return (
    <>
      <h1>{slug}</h1> 
      {slugError && <h1 style={{ color: "red" }}>{slugError}</h1>} 
    </>
  );
}

export default App;
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { useParams } from "react-router-dom";
import { directus } from "./util/directus"; 
import { readItems, updateItem } from "@directus/sdk"; 
import { useEffect, useState } from "react"; 

function App(){...}

function LinkRoute() { 
  const { slug } = useParams();
  const [slugError, setSlugError] = useState(""); 

  useEffect(() => { 
    async function fetchShortLink() { 
      try { 
        const data = await directus.request(readItems("short_link")); 

        const slug_data = data 
          .map((y) => y) 
          .filter((z) => z.slug.toLowerCase().includes(slug)); 

        if (!slug_data || slug_data?.length === 0) { 
          setSlugError(`Invalid Slug: Couldn't find the record →→ ${slug}`); 
          throw new Error("Invalid Slug: Couldn't find that record"); 
        } 
        const shortLink = slug_data[0] as ShortLink; 

        await directus.request( 
          updateItem("short_link", shortLink.id, { 
            clicks: shortLink.clicks + 1, 
          }) 
        ); 

        window.location.assign(`${shortLink.url}`);

      } catch (error) { 
        console.log(error); 
      } 
    } 

    fetchShortLink(); 
  }, [slug]); 

  return (
    <>
      <h1>{slug}</h1> 
      {slugError && <h1 style={{ color: "red" }}>{slugError}</h1>} 
    </>
  );
}

export default App;

The LinkRoute component uses the useParams hook to get the slug parameter from the URL and the useState hook to store an error message if the slug is invalid.

The useEffect hook is used to fetch data from the Directus API via the fetchShortLink effect function, which performs the following actions:

  1. It makes a request to the Directus API to read all short links via the readItems composable.
  2. The response data is filtered to find the short link that matches the current slug.
  3. If no matching short link is found, it sets an error message and throws an error.
  4. If a matching short link is found, it updates the short links click property by 1 via the updateItem composable from Directus API.
  5. Finally, the user is redirected to the URL associated with the short link using the window.location.assign method.

Finally, if an error occurs during the data fetching process, the error is caught and logged to the console. The LinkRoute component returns the error message if the slug is invalid.

Conclusion

You’ve just learned how to set up the data collection process and implement dynamic routing using React and Directus. The broad process involved setting up a Directus data model, customizing user roles and permissions, creating an API endpoint to query data from Directus, and configuring a dynamic route to navigate to slug URLs.

I hope you find this tutorial useful - if you have any questions or hurdles feel free to join the Directus official Discord server.

Make it count

How helpful was this article?