How I made a really fast Link Shortener that runs on the edge

11 min read

demo

I recently made a link shortener called deoxys (named after a really fast Pokémon). It's really, really fast because it uses Vercel 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

High level overview

high level overview

The frontend is built with Next.js which is a full stack React framework. I'm using tRPC as my API layer for that sweet type-safety. I wrote a blog about tRPC if you're not familiar with it. The database is a MySQL database (Vitess 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. It's just a standard Next.js project, I also set up tRPC and Prisma, and connected to my database.

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. I had to do this because the edge function can't use the prisma client. Note that this is a dynamic route.

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

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

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

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 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.

src/components/CreateLink.tsx
const [form, setForm] = useState<Form>({ slug: "", url: "" });

I also called my tRPC endpoints here.

src/components/CreateLink.tsx
const checkSlug = trpc.useQuery(["checkSlug", { slug: form.slug }], {
  refetchOnReconnect: false,
  refetchOnMount: false,
  refetchOnWindowFocus: false,
});

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

Here comes the form.

src/components/CreateLink.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.

src/components/CreateLink.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

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

src/components/CreateLink.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

Here's the code for that.

src/components/CreateLink.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.

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

Credits

Thanks for reading!