Skip to main content
RapidDev - Software Development Agency
how-to-build-v030-60 minutes

How to Build URL shortener app with V0

Build a Bitly-style URL shortener with V0 featuring short link creation, click tracking with analytics (referrer, country, device), and a dashboard with Recharts visualizations. You'll create nanoid-based short codes, 302 redirect handling, and per-link analytics — all in about 30-60 minutes.

What you'll build

  • URL shortening form with shadcn/ui Input and Button that generates unique nanoid short codes
  • 302 redirect handler that logs click metadata (referrer, country, device, browser) before redirecting
  • Per-link analytics page with Recharts BarChart for clicks over time and PieChart for device breakdown
  • Link management dashboard with Table showing all links, click counts, and active/expired status Badge
  • Copy-to-clipboard functionality on generated short URLs
  • Collision-free short code generation with nanoid retry on unique constraint violation
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner9 min read30-60 minutesV0 FreeApril 2026RapidDev Engineering Team
TL;DR

Build a Bitly-style URL shortener with V0 featuring short link creation, click tracking with analytics (referrer, country, device), and a dashboard with Recharts visualizations. You'll create nanoid-based short codes, 302 redirect handling, and per-link analytics — all in about 30-60 minutes.

What you're building

Long URLs are ugly in emails, social posts, and printed materials. A URL shortener creates clean, trackable links that also provide analytics on who clicks them, from where, and on what device.

V0 generates the URL form, redirect handler, and analytics dashboard from prompts. Supabase stores links and click events. Recharts renders the analytics charts. The entire project is simple enough to build in under an hour.

The architecture uses a Server Action for link creation with nanoid, an API route handler for the redirect that logs click data, Server Components for the dashboard, and Recharts client components for analytics visualizations.

Final result

A URL shortener with link creation, click tracking, referrer and device analytics, and a management dashboard with charts.

Tech stack

V0AI Code Generator
Next.jsFull-Stack Framework
Tailwind CSSStyling
shadcn/uiComponent Library
SupabaseDatabase
RechartsData Visualization

Prerequisites

  • A V0 account (free tier works for this project)
  • A Supabase project (free tier works — connect via V0's Connect panel)
  • No additional API keys or accounts needed

Build steps

1

Set up the links and clicks database schema

Open V0 and create a new project. Use the Connect panel to add Supabase. Create the links and clicks tables with a unique constraint on short_code for collision prevention.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Create a Supabase schema for a URL shortener:
3// 1. links table: id (uuid PK), short_code (text UNIQUE NOT NULL), original_url (text NOT NULL), owner_id (uuid FK nullable), title (text), clicks (int DEFAULT 0), created_at (timestamptz), expires_at (timestamptz nullable)
4// 2. clicks table: id (uuid PK), link_id (uuid FK), referrer (text), country (text), city (text), device (text), browser (text), ip_address (inet), clicked_at (timestamptz DEFAULT now())
5// Add index on links.short_code for fast lookups.
6// RLS: public can read links for redirect, authenticated users manage their own links.
7// Generate the SQL migration.

Pro tip: Use V0's prompt queuing — queue the schema prompt, then the URL form component, then the analytics dashboard as three separate prompts.

Expected result: Two tables created with a unique index on short_code and RLS policies for public redirect access and authenticated management.

2

Build the URL shortening form with nanoid generation

Create the main page with an Input for the URL and a Button to shorten it. The Server Action generates a 7-character nanoid short code and retries on collision.

app/actions/links.ts
1'use server'
2
3import { createClient } from '@/lib/supabase/server'
4import { nanoid } from 'nanoid'
5import { revalidatePath } from 'next/cache'
6
7export async function shortenUrl(formData: FormData) {
8 const supabase = await createClient()
9 const originalUrl = formData.get('url') as string
10 const title = formData.get('title') as string
11
12 let shortCode = nanoid(7)
13 let attempts = 0
14
15 while (attempts < 5) {
16 const { error } = await supabase.from('links').insert({
17 short_code: shortCode,
18 original_url: originalUrl,
19 title: title || null,
20 })
21
22 if (!error) {
23 revalidatePath('/')
24 return { shortCode }
25 }
26
27 if (error.code === '23505') {
28 shortCode = nanoid(7)
29 attempts++
30 } else {
31 return { error: error.message }
32 }
33 }
34
35 return { error: 'Failed to generate unique code' }
36}

Expected result: Submitting a URL generates a 7-character short code. If a collision occurs (extremely rare with nanoid), it retries up to 5 times.

3

Create the redirect handler with click tracking

Build an API route that looks up the short code, logs click metadata, increments the click counter, and returns a 302 redirect to the original URL.

app/api/redirect/[code]/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3
4const supabase = createClient(
5 process.env.NEXT_PUBLIC_SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9export async function GET(
10 req: NextRequest,
11 { params }: { params: Promise<{ code: string }> }
12) {
13 const { code } = await params
14
15 const { data: link } = await supabase
16 .from('links')
17 .select('id, original_url, expires_at')
18 .eq('short_code', code)
19 .single()
20
21 if (!link) {
22 return NextResponse.redirect(new URL('/', req.url))
23 }
24
25 if (link.expires_at && new Date(link.expires_at) < new Date()) {
26 return NextResponse.redirect(new URL('/?expired=true', req.url))
27 }
28
29 // Log click asynchronously
30 const userAgent = req.headers.get('user-agent') ?? ''
31 const referrer = req.headers.get('referer') ?? ''
32 const ip = req.headers.get('x-forwarded-for')?.split(',')[0] ?? ''
33
34 supabase.from('clicks').insert({
35 link_id: link.id,
36 referrer,
37 device: /mobile/i.test(userAgent) ? 'mobile' : 'desktop',
38 browser: /chrome/i.test(userAgent) ? 'Chrome' : /firefox/i.test(userAgent) ? 'Firefox' : /safari/i.test(userAgent) ? 'Safari' : 'Other',
39 ip_address: ip,
40 }).then(() => {
41 supabase.rpc('increment_clicks', { p_link_id: link.id })
42 })
43
44 return NextResponse.redirect(link.original_url, { status: 302 })
45}

Expected result: Visiting /api/redirect/abc1234 logs the click and redirects to the original URL with a 302 status. Expired links redirect to the home page.

4

Build the analytics dashboard with Recharts

Create a per-link analytics page showing click trends over time, device breakdown, and referrer sources using Recharts BarChart and PieChart.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a link analytics page at app/links/[id]/page.tsx with:
3// 1. Server Component that fetches the link and all its clicks from Supabase
4// 2. Summary Cards: total clicks, unique visitors (distinct IP), top referrer
5// 3. Recharts BarChart showing clicks per day for the last 30 days
6// 4. Recharts PieChart showing device breakdown (mobile vs desktop)
7// 5. Table showing recent clicks with columns: timestamp, referrer, device, browser, country
8// 6. Badge for active/expired status
9// 7. Copy button to copy the short URL
10// Use shadcn/ui Card for chart containers and Table for click log.

Expected result: An analytics page with summary Cards, a BarChart of clicks over time, PieChart of device breakdown, and a Table of recent individual clicks.

5

Build the link management dashboard

Create a dashboard page showing all the user's shortened links in a Table with click counts, creation dates, and action buttons for copying, viewing analytics, and deleting.

prompt.txt
1// Paste this prompt into V0's AI chat:
2// Build a link management dashboard at app/dashboard/page.tsx with:
3// 1. Server Component fetching all links for the current user
4// 2. shadcn/ui Table with columns: Short URL (clickable), Original URL (truncated), Clicks, Created, Status Badge (active/expired)
5// 3. Each row has a copy button (copies short URL to clipboard) and a link to analytics
6// 4. "Shorten URL" Button at the top that opens a Dialog with Input for URL and optional title
7// 5. Delete Button with AlertDialog confirmation
8// 6. Summary Cards at top: total links, total clicks, clicks today
9// Use Badge variant 'default' for active links and 'destructive' for expired.

Expected result: A dashboard Table showing all shortened links with click counts, status Badges, copy buttons, analytics links, and summary Cards at the top.

Complete code

app/api/redirect/[code]/route.ts
1import { NextRequest, NextResponse } from 'next/server'
2import { createClient } from '@supabase/supabase-js'
3
4const supabase = createClient(
5 process.env.NEXT_PUBLIC_SUPABASE_URL!,
6 process.env.SUPABASE_SERVICE_ROLE_KEY!
7)
8
9export async function GET(
10 req: NextRequest,
11 { params }: { params: Promise<{ code: string }> }
12) {
13 const { code } = await params
14
15 const { data: link } = await supabase
16 .from('links')
17 .select('id, original_url, expires_at')
18 .eq('short_code', code)
19 .single()
20
21 if (!link) {
22 return NextResponse.redirect(new URL('/', req.url))
23 }
24
25 if (link.expires_at && new Date(link.expires_at) < new Date()) {
26 return NextResponse.redirect(new URL('/?expired=true', req.url))
27 }
28
29 const userAgent = req.headers.get('user-agent') ?? ''
30 const referrer = req.headers.get('referer') ?? ''
31
32 supabase
33 .from('clicks')
34 .insert({
35 link_id: link.id,
36 referrer,
37 device: /mobile/i.test(userAgent) ? 'mobile' : 'desktop',
38 browser: /chrome/i.test(userAgent)
39 ? 'Chrome'
40 : /safari/i.test(userAgent)
41 ? 'Safari'
42 : 'Other',
43 })
44 .then(() =>
45 supabase.rpc('increment_clicks', { p_link_id: link.id })
46 )
47
48 return NextResponse.redirect(link.original_url, { status: 302 })
49}

Customization ideas

Add custom short codes

Let users choose their own short code (e.g., /my-brand) instead of random nanoid. Validate uniqueness and length restrictions.

Add QR code generation

Generate a QR code for each short link using a library like qrcode.react. Display it on the analytics page with a download button.

Add link expiration settings

Let users set expiration dates on links. Show a countdown on the dashboard and redirect expired links to a custom 'link expired' page.

Add UTM parameter builder

Add a form that helps users append UTM parameters (source, medium, campaign) to their original URL before shortening for marketing tracking.

Common pitfalls

Pitfall: Using sequential IDs or short random strings for short codes

How to avoid: Use nanoid(7) which generates URL-safe random strings with 2B+ combinations. Retry on unique constraint violation to handle the rare collision.

Pitfall: Logging clicks synchronously before redirecting

How to avoid: Fire the click insert as a fire-and-forget promise. The redirect returns immediately while the click logs asynchronously.

Pitfall: Not handling expired links

How to avoid: Check expires_at in the redirect handler and redirect expired links to a friendly 'link expired' page instead of the original URL.

Best practices

  • Use nanoid(7) for short code generation — URL-safe, collision-resistant, and compact enough for short URLs
  • Log clicks asynchronously (fire-and-forget) so the redirect response is instant without database latency
  • Use V0's prompt queuing to build the URL form, redirect handler, and analytics dashboard as three queued prompts
  • Create an atomic increment_clicks RPC function in Supabase to prevent count drift from concurrent clicks
  • Use Design Mode (Option+D) to visually polish the URL form and analytics charts at zero credit cost
  • Set up a custom domain in Vercel project settings for branded short links (e.g., yourbrand.link/abc1234)
  • Store the short link domain as an environment variable so it works correctly in both development and production

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm building a URL shortener with Next.js App Router and Supabase. I need: 1) Short code generation with nanoid and collision retry, 2) A redirect handler that logs clicks asynchronously, 3) Click analytics with referrer, device, and time data, 4) Recharts charts for the analytics dashboard. Help me design the schema and the async click logging pattern.

Build Prompt

Create a URL redirect handler at app/api/redirect/[code]/route.ts that: 1) Looks up the short_code in Supabase links table, 2) Returns 302 redirect to original_url, 3) Logs click metadata (referrer, user-agent parsed device/browser) asynchronously without blocking the redirect, 4) Handles missing links with redirect to home page, 5) Handles expired links. Use fire-and-forget Supabase insert for click logging.

Frequently asked questions

How are short codes generated?

Short codes are generated using nanoid(7), which creates 7-character URL-safe random strings. With 2 billion possible combinations, collisions are extremely rare. If a collision occurs, the Server Action retries with a new nanoid up to 5 times.

Can I use a custom domain for short links?

Yes. Add a custom domain in your Vercel project settings (e.g., yourbrand.link). Then set NEXT_PUBLIC_SHORT_DOMAIN in V0's Vars tab so the UI displays the correct short URL.

What V0 plan do I need?

V0 Free tier works perfectly. The URL shortener is a simple project with basic Server Components, one API route, and shadcn/ui components.

How accurate are the analytics?

Click tracking captures referrer, user agent (parsed into device and browser), and IP address. Country and city detection requires a third-party IP geolocation API which can be added as an enhancement.

How do I deploy this?

Click Share > Publish in V0. The Supabase connection is auto-configured. Short links start working immediately with the Vercel production URL.

Can RapidDev help build a custom link management platform?

Yes. RapidDev has built 600+ apps including marketing platforms with link tracking, UTM management, and conversion analytics. Book a free consultation to discuss your link management needs.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.