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:
Uniquely identify a post or record
Redirect to the correct full/original URL
Allow multiple different short URLs for the same post
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:
Inserts a row containing the full redirect URL
Reads the inserted row’s ID
Encodes that ID using Base62
Pads the string to a fixed length (optional)
Updates the row with the generated
short_urlReturns 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:
Extract the short code from the URL
Look it up in the
urlstableLog the visit time
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
Receive the short code from the URL
Query the database
Drizzle always returns an array, even for one record
If found → log the visit timestamp
Redirect the user
If the short URL doesn’t exist, simply return a 404.
