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
- Next.js
- Prisma
- PlanetScale
- tRPC
- TypeScript (of course)
- TailwindCSS
- Vercel
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.
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.
const [form, setForm] = useState<Form>({ slug: "", url: "" });
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"]);
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>
)}
{/* ... */}
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
/>
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.
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();
}}
/>
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.
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>
);
}
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)
I'm so sorry I literally forgot, fixed now
🥰
hope you liked it!
Fantastic work Shoubhit! Very inspiring to see a fellow Scrimba student make their own awesome personal projects.
thank you!
scrimba is amazing
Awesome job Nexxel 🤓
thanks!