Skip to main content

Command Palette

Search for a command to run...

Create a Tinyurl, short shareable URL without any shortner packages on your own.

Updated
4 min read
Create a Tinyurl, short shareable URL without any shortner packages on your own.

I wrote the entire post myself and then used AI to redirect so no doubt on the originality of the post

Building Short, Shareable URLs in Your Application (Base62 + Drizzle ORM)

When building modern applications, long URLs can look messy — especially when sharing them on social media or messaging apps. For example, your post URL might currently look like:

http://localhost:3000/users/posts/?postid=2

or

https://yourdomain.com/users/posts/?postid=2

This exposes unnecessary route structure like /users/posts/. Instead, wouldn’t it be much cleaner if you could share something like:

https://yourdomain.com/f4sxdx

Just like GoFundMe, each post can have multiple short, unique URLs generated any time a user clicks Share.

In this guide, we’ll walk through how to build this entire system using:

  • Base62 encoding for generating short codes

  • Drizzle ORM

  • Next.js API Routes

  • PostgreSQL


🎯 What We Need to Achieve

Each short URL must:

  1. Uniquely identify a post or record

  2. Redirect to the correct full/original URL

  3. Allow multiple different short URLs for the same post

  4. Store visit history for analytics

We’ll use the popular library base62-ts:
https://www.npmjs.com/package/base62-ts

Base62 encoding converts the numeric ID from the database (1, 2, 3…) into a compact, shareable string like:
0 → 0, 10 → A, 61 → z, 125 → 21, etc.


🗄️ Step 1 — Create the Database Table

We’ll store each short link in its own row:

import { serial, varchar, integer, timestamp, jsonb } from "drizzle-orm/pg-core";
import { pgTable } from "drizzle-orm/pg-core";
import { posts } from "./posts";

export const urls = pgTable("urls", {
  id: serial("id").primaryKey(),
  short_url: varchar("short_url"),
  redirect_url: varchar("redirect_url"),
  visited_history: jsonb("visited_history").$type<string[]>().default([]),
  post_id: integer("post_id")
    .notNull()
    .references(() => posts.id, { onDelete: "cascade" }),
  created_at: timestamp("created_at").defaultNow().notNull()
});

Meaning of each column:

  • short_url → The Base62 + padded string (e.g., /fundA12)

  • redirect_url → The original full URL

  • visited_history → Array of timestamps (useful for analytics)

  • post_id → Foreign key to your posts table

  • id → Auto-incremented number we will encode


⚙️ Step 2 — Generate a Short URL

When the user triggers Share, we hit a POST endpoint that:

  1. Inserts a row containing the full redirect URL

  2. Reads the inserted row’s ID

  3. Encodes that ID using Base62

  4. Pads the string to a fixed length (optional)

  5. Updates the row with the generated short_url

  6. Returns the new short URL

Ready-to-use API code:

import { NextRequest, NextResponse } from "next/server";
import base62 from "base62-ts";
import { db } from "@/lib/drizzle-client";
import { urls } from "@/db/schema/urls";
import { eq } from "drizzle-orm";

export async function POST(req: NextRequest) {
  const reqPostID = await req.json();
  const { postID } = reqPostID;

  if (!postID) {
    return NextResponse.json(
      { error: "Post ID is required" },
      { status: 400 }
    );
  }

  // Construct the original redirect URL
  const redirect_url = `${process.env.BASE_URL}/donate/${postID}`;

  try {
    // Insert row and get the unique auto-incremented ID
    const [row] = await db
      .insert(urls)
      .values({ redirect_url, post_id: postID })
      .returning({ insertedId: urls.id });

    const insertedId = Number(row.insertedId);

    // Encode the ID using Base62
    let encoded62 = base62.encode(insertedId);
    encoded62 = encoded62.toString();

    // Ensure a fixed length (e.g., 6 chars)
    const encoded = encoded62.padStart(6, "fund");

    // Update with the short URL
    await db
      .update(urls)
      .set({ short_url: `/${encoded}` })
      .where(eq(urls.id, insertedId));

    return NextResponse.json({
      message: "Short URL created",
      shortURL: `/${encoded}`
    });
  } catch (error) {
    return NextResponse.json(
      { error: error instanceof Error ? error.message : "Unknown error" },
      { status: 500 }
    );
  }
}

Why padStart(6, "fund")?

padStart ensures that even tiny Base62 values (like "1") become 6-character strings:

"1""fundf1"
"23""fund23"
"i8""fundi8"

This makes URLs consistent and harder to guess.


🔗 Step 3 — Redirect Short URL → Original URL

Your redirect route must:

  1. Extract the short code from the URL

  2. Look it up in the urls table

  3. Log the visit time

  4. Redirect the user to the real page

Example:

interface ResponseData {
  id: number;
  short_url: string | null;
  redirect_url: string | null;
  visited_history: string[] | null;
  created_at: Date;
}

export async function GET(
  req: NextRequest,
  { params }: { params: Promise<{ shortURL: string }> }
) {
  const { shortURL } = await params;

  try {
    const result: ResponseData[] = await db
      .select()
      .from(urls)
      .where(eq(urls.short_url, `/${shortURL}`));

    if (result.length === 0) {
      return NextResponse.json({ message: "No URL found" }, { status: 404 });
    }

    const row = result[0];

    // Append visit timestamp
    const updatedHistory = [
      ...(row.visited_history || []),
      new Date().toISOString()
    ];

    await db
      .update(urls)
      .set({ visited_history: updatedHistory })
      .where(eq(urls.id, row.id));

    if (!row.redirect_url) {
      return NextResponse.json(
        { message: "Invalid redirect URL" },
        { status: 500 }
      );
    }

    return NextResponse.redirect(row.redirect_url);
  } catch (error) {
    return NextResponse.json(
      { message: "Unexpected error" },
      { status: 500 }
    );
  }
}

🧠 Summary of the Redirect Logic

  1. Receive the short code from the URL

  2. Query the database

  3. Drizzle always returns an array, even for one record

  4. If found → log the visit timestamp

  5. Redirect the user

If the short URL doesn’t exist, simply return a 404.