DEV Community

Cover image for How I made a really fast Link Shortener that runs on the edge
Shoubhit Dash
Shoubhit Dash

Posted on • Edited on

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

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])
}
Enter fullscreen mode Exit fullscreen mode

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;
};
Enter fullscreen mode Exit fullscreen mode

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);
  }
}
Enter fullscreen mode Exit fullscreen mode

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,
});
Enter fullscreen mode Exit fullscreen mode

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.

const [form, setForm] = useState<Form>({ slug: "", url: "" });
Enter fullscreen mode Exit fullscreen mode

I also called my tRPC endpoints here.

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

const createShortLink = trpc.useMutation(["createShortLink"]);
Enter fullscreen mode Exit fullscreen mode

Here comes the form.

<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>
      )}
{/* ... */}
Enter fullscreen mode Exit fullscreen mode

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.

<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
/>
Enter fullscreen mode Exit fullscreen mode

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.

<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();
    }}
/>
Enter fullscreen mode Exit fullscreen mode

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.

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>
    );
  }
Enter fullscreen mode Exit fullscreen mode

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.

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

Credits

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

Thanks for reading!

Top comments (8)

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
nexxeln profile image
Shoubhit Dash

I'm so sorry I literally forgot, fixed now

Collapse
 
yxsh profile image
Yash

🥰

Collapse
 
nexxeln profile image
Shoubhit Dash

hope you liked it!

Collapse
 
webdevterri profile image
webdevterri

Fantastic work Shoubhit! Very inspiring to see a fellow Scrimba student make their own awesome personal projects.

Collapse
 
nexxeln profile image
Shoubhit Dash

thank you!
scrimba is amazing

Collapse
 
dreyfus92 profile image
Paul Valladares

Awesome job Nexxel 🤓

Collapse
 
nexxeln profile image
Shoubhit Dash

thanks!