Making a link shortener with Astro DB

To easily share my Spotify, I like to use link shortener services. I got tired of how they always stopped working, so like any other sane person, I wrote my own.

A repeating pattern of the words "Link Shortener" and the logo, which looks like a brick wall of sort.

To see it in action, visit my Spotify profile at

I like sharing music. To follow my friends and stalk see what they’re listening to, I often send them my Spotify profile. However, ever since they removed custom usernames, I’ve been stuck with the very memorable username of zhwq0rxdn060sgar22e07u193, and searching for my display name is pretty much impossible.

To get around that, I use link shorteners, but I’ve found that they’re very unreliable, and often shut down/stop working. With the recent release of Astro Studio and Astro DB, I thought this was the perfect chance to test it out… and solve my issue once and for all.

Setting up the project

First I had to connect with Astro Studio and also add the Astro DB integration. Both of these were relatively simple to set up. Studio integration is explained well, and even examples are provided for how to work with the DB for people that don’t know SQL (me).

One thing that wasn’t too clear was where I should create the config.ts file. It turns out that it should be placed in a db folder outside of your src folder. Otherwise, local type definitions weren’t being generated for me in the .astro folder.

My project only has two routes: a main index.astro for the root, and a [...slug].astro file that captures everything else. The former is for creating new links, and the latter is where we do the redirecting.

The database schema is extremely simple: there are three columns, with the short link as the primary key, the original URL, and the date that it was generated. Astro DB makes it very easy to declare the schema. This is my entire config.ts:

import { column, defineDb, defineTable, NOW } from "astro:db";

const Link = defineTable({
  columns: {
    short: column.text({ primaryKey: true }),
    original: column.text(),
    created:{ default: NOW }),

export default defineDb({
  tables: {

There were many ways to do this, but I wanted to play with forms and Astro’s API routes, so that’s exactly what I did. The user inputs the original URL and upon submitting the form, I make a fetch call to /api/create that handles the generation. To update the UI (e.g. displaying the newly generated link, or showing an error message) I use React to keep track of state.

The backend receives a POST request from the form, with the form data as the body. I use nanoid which generates a random string of characters for me. I’m using an alphanumeric alphabet with a size of 7.

After performing input validation with Zod, I insert the values into the database, and return a response with a 2xx code to signify everything went well. I also return the newly generated short link in the response body for the UI to consume.

Checking for collisions and errors

Any random generator will have a probability of a collision, and this calculator estimates that 266,000 short links need to be generated before we reach a one percent probability of a collision. This is a very tiny number, and if it happens I should probably buy a lottery ticket, but I check anyway by querying the database:

let short;

while (true) {
  short = nanoid();
  const query = await, short));

  if (query.length === 0) break;

I also make sure to handle errors (wrapping stuff in try-catch) and return a 5xx response if things go wrong, but I don’t show that here.


All potential redirects are handled in [...slug].astro inside the frontmatter. We first check if the slug in the current URL exists in the database. I select all the rows where the short link matches the slug (and there should only be one row), and from that row I extract the original URL.

If no short URL exists in the database, then I simply redirect to the home page.

const { slug } = Astro.params;

if (!slug) {
  return Astro.redirect("/");

const query = await, slug));
const exist = query.length === 1;

if (!exist) {
  return Astro.redirect("/");

Astro has a built-in way of dynamically modifying the response returned by the server. In particular, I set the status code to 301 and set the Location header to the original URL.

Astro.response.status = 301;
Astro.response.headers.set("Location", originalUrl);

I spent a bit trying to figure out which redirect status code I should be responding with. Doing a bit of research revealed that most link shorteners use either 301 or 302. I settled on 301 because

  • once a short URL is generated, it will forever be associated with the original URL. “Moved permanently” perfectly captures this idea.

  • the class of temporary redirections doesn’t fit our motives here. Temporary implies that some day, a user will be able to access the resources/page at our short URL. This will never happen; in fact, there is literally no HTML to render or present. More on this below.

  • search engines and crawlers should never show the short URL.

One thing that is interesting to me is that I wrote no JSX for the /[...slug] route. The only code in [...slug].astro is in the Astro frontmatter, which only gets executed on the server. While Astro was originally launched with static generation in mind, we’ve come a long way, and from the way I’ve been using it, it feels totally possible to do things that are usually delegated to a “full-stack” framework.

Styling and frontend

Choosing colors is hard, so I used Randoma11y to generate an accessible color palette for me. I wanted to keep things simple, so the whole site is just two colors and one font, and I picked Commit Mono because it’s a nice neutral and readable monospaced font.

The rest of the site is pretty standard, but while making it I noticed one tiny, small, insignificant detail. It occurred whenever I clicked on the “Copy to clipboard” button:

The button changes width! That’s so annoying! And not really important! But I had to fix it. The solution I came up with was to place two <span> tags inside the <button>, and when it was pressed, it would hide “Copy to clipboard” and show the other. It looks like this:

<button className="relative">
  <span className="absolute inset-x-0" hidden>
  <span style={{ opacity: "100" }}>Copy to clipboard</span>

The key part is that “Copy to clipboard” remains in the DOM tree; I’m just hiding it by setting the opacity to zero. This allows the button to maintain its width even when the text changes. In order to center “Copied,” I had to position it absolutely, to account for the fact that the other <span> tag was still inside the button.

I then attached an event listener to the button and used setTimeout() to revert the changes after a short duration. Now the button width remains the same:


Unless Astro Studio shuts down anytime soon, my link shortener should continue to work. Maybe this time around, my Spotify profile link will last more than a couple of weeks. I hope.

I might add a few more features in the future. Some stuff I have in mind include

  • letting users turn off numbers, capital letters, etc. in their short link

  • customizing the length of the short link (but I think 7 will always be the max)

  • letting users enter a custom short link (instead of being randomly generated)

  • showing an error or “not found” page instead of redirecting to home

  • a fun button that cycles through other colors from Randoma11y

  • setting expiration dates for links. I don’t have too much control over the server (this project is hosted on Vercel), so I might just lazily check (only check if the link is actually accessed).

So if you’re visiting the site in the future and see anything I didn’t describe here, then I probably got around to doing it. The source code for this project is available on GitHub and to try it out, visit