How I made a really fast Link Shortener that runs on the edge
Published: 2022-06-01 04:48:32

![demo](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_7QyixVB0nP.gif)

I recently made a link shortener called [deoxys](https://deoxys.nexxel.dev) (named after a really fast Pokémon). It's really, really fast because it uses [Vercel Edge Functions](https://vercel.com/features/edge-functions). Edge functions are basically functions that run on the cloud, so they are really fast and have no cold starts, and everything runs on the server so there is zero client side burden. In this blog I'm going to give you an overview of the architecture of deoxys.

## Stack
- [Next.js](https://nextjs.org)
- [Prisma](https://prisma.io)
- [PlanetScale](https://planetscale.com)
- [tRPC](https://trpc.io)
- [TypeScript](https://typescriptlang.org) (of course)
- [TailwindCSS](https://tailwindcss.com)
- [Vercel](https://vercel.com)

## High level overview

![high level overview](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_pySo8pWQHC.png)

The frontend is built with Next.js which is a full stack [React](https://reactjs.org) framework. I'm using tRPC as my API layer for that sweet type-safety. I wrote a [blog about tRPC](https://deoxys.nexxel.dev/trpc-blog) if you're not familiar with it. The database is a MySQL database ([Vitess](https://vitess.io/) to be precise) provided by PlanetScale.

Whenever someone shortens a new link, the frontend calls a tRPC mutation to store that in the database. The ORM I'm using is Prisma, because it is simply the best. 

Now here comes the interesting part, whenever someone visits a shortened URL, lets say `https://deoxys.nexxel.dev/cat`, it will run an edge function to check if the provided slug (in this case *cat*), is a valid slug, if it is, it will redirect the user to whatever the URL was.

## Code walkthrough
You can look at the source code [here](https://github.com/nexxeln/deoxys). It's just a standard Next.js project, I also set up tRPC and Prisma, and connected to my database. 

```prisma
// prisma/schema.prisma

model ShortLink {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  url       String   @db.VarChar(3000)
  slug      String   @unique

  @@index([slug])
}
```

This is the schema for the database. Very simple and minimal. Next, I made the API endpoint that will check if a slug is valid or not. For this I used a [Next.js API Route](https://nextjs.org/docs/api-routes/introduction). I had to do this because the edge function can't use the prisma client. Note that this is a [dynamic route](https://nextjs.org/docs/api-routes/dynamic-api-routes). 

```ts
//src/pages/api/get-link/[slug].ts

import type { NextApiRequest, NextApiResponse } from "next";

import { prisma } from "../../../db/client";

export default async (req: NextApiRequest, res: NextApiResponse) => {
  const slug = req.query["slug"];

  if (!slug || typeof slug !== "string") {
    res.status(404).json({ message: "please provide a slug" });

    return;
  }

  const data = await prisma.shortLink.findFirst({
    where: {
      slug: {
        equals: slug,
      },
    },
  });

  if (!data) {
    res.status(404).json({ message: "short link not found" });

    return;
  }

  res.setHeader("Content-Type", "application/json");
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Cache-Control", "s-maxage=1000000000, stale-while-revalidate");

  res.json(data);

  return;
};
```

If the slug is valid, it is also caching the response for 1000000000 seconds. This is what makes the edge function even faster.

Next, I wrote my edge function, in Next.js, edge functions are written in `pages/_middleware.ts`

```ts
// src/pages/_middleware.ts

import { NextFetchEvent, NextRequest, NextResponse } from "next/server";

export async function middleware(req: NextRequest, event: NextFetchEvent) {
  if (
    req.nextUrl.pathname.startsWith("/api/") ||
    req.nextUrl.pathname === "/"
  ) {
    return;
  }
  const slug = req.nextUrl.pathname.split("/").pop();

  const fetchSlug = await fetch(`${req.nextUrl.origin}/api/get-link/${slug}`);

  if (fetchSlug.status === 404) {
    return NextResponse.redirect(req.nextUrl.origin);
  }

  const data = await fetchSlug.json();

  if (data?.url) {
    return NextResponse.redirect(data.url);
  }
}
```

It calls that endpoint and checks if the slug is valid, if it is it redirects the user to the URL corresponding to the slug. That's pretty much it.

Now I built a nice UI for it using Tailwind. I also made two tRPC endpoints. The first one is to check if a slug has been previously used before in real-time. I find this real-time validation to be really cool. Look at this.

![real-time validation](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_0r37qjkR6I.gif)

The second endpoint is to create new links and write it to the database. The code looks like this.

```ts
// src/pages/api/trpc/[trpc].ts

import * as trpc from "@trpc/server";
import * as trpcNext from "@trpc/server/adapters/next";
import { z } from "zod";

import { prisma } from "../../../db/client";

export const appRouter = trpc
  .router()
  .query("checkSlug", {
    input: z.object({ slug: z.string() }),
    async resolve({ input }) {
      const slugCount = await prisma.shortLink.count({
        where: {
          slug: {
            equals: input.slug,
          },
        },
      });

      return { used: slugCount > 0 };
    },
  })
  .mutation("createShortLink", {
    input: z.object({ slug: z.string(), url: z.string() }),
    async resolve({ input }) {
      try {
        await prisma.shortLink.create({
          data: {
            slug: input.slug,
            url: input.url,
          },
        });
      } catch (error) {
        console.log(error);
      }
    },
  });

export type AppRouter = typeof appRouter;

export default trpcNext.createNextApiHandler({
  router: appRouter,
  createContext: () => null,
});
```

I'm also using [zod](https://zod.dev/) for input validation here. Really good library.

The rest was simple, I just made a form component that called my tRPC endpoints. First I declared some state for the form.

```ts
const [form, setForm] = useState<Form>({ slug: "", url: "" });
```

I also called my tRPC endpoints here.

```ts
const checkSlug = trpc.useQuery(["checkSlug", { slug: form.slug }], {
    refetchOnReconnect: false,
    refetchOnMount: false,
    refetchOnWindowFocus: false,
  });

const createShortLink = trpc.useMutation(["createShortLink"]);
```

Here comes the form.

```tsx
<form
      onSubmit={(event: React.FormEvent<HTMLFormElement>) => {
        event.preventDefault();
        createShortLink.mutate({ ...form });
      }}
      className="mt-6"
    >
      {checkSlug.data?.used ? (
        <span className="font-medium text-center text-red-500">
          This link has already been used
        </span>
      ) : (
        <span className="font-medium text-center">
          {url}/{form.slug}
        </span>
      )}
{/* ... */}
```

Here, I'm passing an `onSubmit` function to the form that calls that tRPC mutation and passes the form state in the input. Also this is where I'm actually implementing that real-time validation, if the endpoint returns `used` as true, it will make the border red and show the error message.

Inside the form there are just a bunch of inputs, here is how they work.

```tsx
<input
    type="url"
    value={form.url}
    maxLength={3000}
    onChange={(e) => setForm({ ...form, url: e.target.value })}
    placeholder="https://duckduckgo.com"
    className="block w-full px-4 py-2 font-normal bg-black border-2 border-gray-200 rounded-md focus:outline-none placeholder:text-gray-400"
    required
/>
```


This input is for the URL that has to be shortened, here I'm passing an `onChange` function to set my form state. Also the `type="url"` helps in validation.

![validation](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_tfDkPVfPaL.png)

For random slugs, I'm using a library called random-word-slugs, it's pretty cool. Here's the code for the random button.

```tsx
<input
    type="button
    value="Random"
    className="px-4 py-2 ml-2 font-medium transition-colors duration-300 bg-indigo-500 border-2 border-indigo-500 rounded cursor-pointer hover:bg-transparent"
    onClick={() => {
        const slug = generateSlug();

        setForm({
            ...form,
            slug,
        });

        checkSlug.refetch();
    }}
/>
```

The `generateSlug()` function comes from the random-word-slugs library. I'm also setting the state, and checking if that particular slug has already been used before.

Now if the creation of the short link was successful, it shows this page.

![succesful creation](https://us-east-1.tixte.net/uploads/nexxel.needs.rest/brave_JxlmBMr9P7.png)

Here's the code for that.

```tsx
if (createShortLink.status === "success") {
    return (
      <div className="flex flex-col items-center justify-center mx-3 mt-6">
        <span className="pb-3 text-lg font-semibold">Here's your link!</span>

        <div className="flex items-center gap-2">
          <h1 className="text-lg text-center md:text-2xl">{`${url}/${form.slug}`}</h1>
          <button
            className="px-4 py-1.5 ml-3 font-medium transition-colors duration-300 bg-indigo-500 border-2 border-indigo-500 rounded hover:bg-transparent"
            onClick={() => {
              copy(`${url}/${form.slug}`);
            }}
          >
            Copy
          </button>
        </div>

        <button
          className="px-4 mt-8 py-1.5 ml-3 font-medium transition-colors duration-300 bg-indigo-500 border-2 border-indigo-500 rounded hover:bg-transparent"
          onClick={() => {
            createShortLink.reset();
            setForm({ slug: "", url: "" });
          }}
        >
          Create New
        </button>
      </div>
    );
  }
```

tRPC returns the status of a mutation too. So here, if it returns `success`, it shows the shortened URL and a copy to clipboard button. There is also a create new button that resets the tRPC mutation and resets the form state as well. 

You can see the full code for this component [here](https://github.com/nexxeln/deoxys/blob/main/src/components/CreateLink.tsx).

That's it. There are a lot of moving parts to this, I hope I gave you nice overview of how deoxys functions.

Website: https://deoxys.nexxel.dev
Code: https://github.com/nexxeln/deoxys

## Credits
I'm so sorry I did not include this before.

- [Theo](https://www.youtube.com/c/TheoBrowne1017)
- [Christian](https://github.com/heyglassy)
- [Joltik](https://github.com/TheoBr/joltik)

Thanks for reading!