URL Shortener with Next.js Route Handlers + Prisma

~ Index Introduction Tech Stack & Setup Schema & Database API Endpoint to Shorten URLs Route Handler to Redirect Optional UI with Client-side Form Bonus Tips Conclusion ~ Introduction Ever sent someone a massive link that looks like it came out of a horror movie script? It’s 2025. Links should be short and clean. In this post, we’ll build a fully working URL shortener using the new Next.js App Router, Route Handlers, and Prisma with a SQLite database. The whole thing fits into just a few files. No backend frameworks, no third-party services, just pure Next.js magic. Let’s dive in!!! ~ Tech Stack & Setup We’re keeping things light but modern: Next.js (App Router) TypeScript Prisma (ORM) SQLite (because dev should be fast) Init your project npx create-next-app@latest url-shortener --experimental-app --typescript cd url-shortener Install dependencies npm install prisma @prisma/client npx prisma init Configure .env DATABASE_URL="file:./dev.db" NEXT_PUBLIC_BASE_URL="http://localhost:3000" ~ Schema & Database Open prisma/schema.prisma and drop this in: generator client { provider = "prisma-client-js" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } model Link { id Int @id @default(autoincrement()) slug String @unique url String createdAt DateTime @default(now()) } Create the database npx prisma migrate dev --name init That’s your backend. Done. ~ API Endpoint to Shorten URLs File: app/api/shorten/route.ts import { NextRequest, NextResponse } from 'next/server'; import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); function generateSlug(length = 6) { const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; return Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]).join(''); } export async function POST(request: NextRequest) { const { url } = await request.json(); if (!url) return NextResponse.json({ error: 'Missing URL' }, { status: 400 }); let slug = generateSlug(); while (await prisma.link.findUnique({ where: { slug } })) { slug = generateSlug(); } const record = await prisma.link.create({ data: { url, slug } }); const shortUrl = `${process.env.NEXT_PUBLIC_BASE_URL}/${record.slug}`; return NextResponse.json({ slug, shortUrl }); } ~ Route Handler to Redirect File: app/[slug]/route.ts import { NextRequest, NextResponse } from 'next/server'; import { PrismaClient } from '@prisma/client'; const prisma = new PrismaClient(); export async function GET(request: NextRequest, { params }: { params: { slug: string } }) { const record = await prisma.link.findUnique({ where: { slug: params.slug } }); if (!record) return new NextResponse('Not found', { status: 404 }); return NextResponse.redirect(record.url); } Your links are now redirecting!!!!!!!!!!! ~ Optional UI with Client-side Form You can just throw in a quick form on your homepage: 'use client'; import { useState } from 'react'; export default function Home() { const [url, setUrl] = useState(''); const [short, setShort] = useState(''); const handleSubmit = async (e: any) => { e.preventDefault(); const res = await fetch('/api/shorten', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }), }); const data = await res.json(); setShort(data.shortUrl); }; return ( URL Shortener setUrl(e.target.value)} placeholder="https://example.com" className="border p-2 rounded" /> Shorten {short && Short URL: {short}} ); } It then should look like this: ~ Bonus Tips Little things to level up: 1. Custom Slugs Want users to pick their own slugs (like mycoolsite.dev/bio)? Just modify the API to accept an optional slug and check if it’s taken. 2. Analytics Tracking Add a clicks field in the model, then increment it on redirect. You’ll have your own tiny analytics dashboard. 3. Deploy with Vercel + PlanetScale (if scaling) SQLite is fine for local dev, but for prod, go MySQL with PlanetScale or NeonDB for Postgres. ~ Conclusion That’s it! You just built a fully working URL shortener with: App Router Route Handlers Prisma ORM SQLite DB One-click redirect magic Optional UI You are now ready to work for google (homeless shelter) Wanna add auth? Rate limiting? QR codes? Let me know! Got questions or ideas? Drop ‘em below. You know what to do. NOW! ~ Luan

Apr 24, 2025 - 19:35
 0
URL Shortener with Next.js Route Handlers + Prisma

~ Index

  • Introduction
  • Tech Stack & Setup
  • Schema & Database
  • API Endpoint to Shorten URLs
  • Route Handler to Redirect
  • Optional UI with Client-side Form
  • Bonus Tips
  • Conclusion

~ Introduction

Ever sent someone a massive link that looks like it came out of a horror movie script?

It’s 2025. Links should be short and clean.

In this post, we’ll build a fully working URL shortener using the new Next.js App Router, Route Handlers, and Prisma with a SQLite database. The whole thing fits into just a few files.

No backend frameworks, no third-party services, just pure Next.js magic.

Let’s dive in!!!

~ Tech Stack & Setup

We’re keeping things light but modern:

  • Next.js (App Router)
  • TypeScript
  • Prisma (ORM)
  • SQLite (because dev should be fast)

Init your project

npx create-next-app@latest url-shortener --experimental-app --typescript
cd url-shortener

Install dependencies

npm install prisma @prisma/client
npx prisma init

Configure .env

DATABASE_URL="file:./dev.db"
NEXT_PUBLIC_BASE_URL="http://localhost:3000"

~ Schema & Database

Open prisma/schema.prisma and drop this in:

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model Link {
  id        Int      @id @default(autoincrement())
  slug      String   @unique
  url       String
  createdAt DateTime @default(now())
}

Create the database

npx prisma migrate dev --name init

That’s your backend. Done.

~ API Endpoint to Shorten URLs

File: app/api/shorten/route.ts

import { NextRequest, NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

function generateSlug(length = 6) {
  const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
  return Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]).join('');
}

export async function POST(request: NextRequest) {
  const { url } = await request.json();

  if (!url) return NextResponse.json({ error: 'Missing URL' }, { status: 400 });

  let slug = generateSlug();
  while (await prisma.link.findUnique({ where: { slug } })) {
    slug = generateSlug();
  }

  const record = await prisma.link.create({ data: { url, slug } });
  const shortUrl = `${process.env.NEXT_PUBLIC_BASE_URL}/${record.slug}`;

  return NextResponse.json({ slug, shortUrl });
}

~ Route Handler to Redirect

File: app/[slug]/route.ts

import { NextRequest, NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export async function GET(request: NextRequest, { params }: { params: { slug: string } }) {
  const record = await prisma.link.findUnique({ where: { slug: params.slug } });
  if (!record) return new NextResponse('Not found', { status: 404 });
  return NextResponse.redirect(record.url);
}

Your links are now redirecting!!!!!!!!!!!

~ Optional UI with Client-side Form

You can just throw in a quick form on your homepage:

'use client';
import { useState } from 'react';

export default function Home() {
  const [url, setUrl] = useState('');
  const [short, setShort] = useState('');

  const handleSubmit = async (e: any) => {
    e.preventDefault();
    const res = await fetch('/api/shorten', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ url }),
    });
    const data = await res.json();
    setShort(data.shortUrl);
  };

  return (
    <main className="flex flex-col items-center justify-center h-screen p-4">
      <h1 className="text-2xl mb-4">URL Shortenerh1>
      <form onSubmit={handleSubmit} className="flex space-x-2">
        <input
          type="url"
          required
          value={url}
          onChange={(e) => setUrl(e.target.value)}
          placeholder="https://example.com"
          className="border p-2 rounded"
        />
        <button type="submit" className="bg-blue-500 text-white p-2 rounded">
          Shorten
        button>
      form>
      {short && <p className="mt-4">Short URL: <a href={short}>{short}a>p>}
    main>
  );
}

It then should look like this:
The UI

~ Bonus Tips

Little things to level up:

1. Custom Slugs

Want users to pick their own slugs (like mycoolsite.dev/bio)?

Just modify the API to accept an optional slug and check if it’s taken.

2. Analytics Tracking

Add a clicks field in the model, then increment it on redirect.

You’ll have your own tiny analytics dashboard.

3. Deploy with Vercel + PlanetScale (if scaling)

SQLite is fine for local dev, but for prod, go MySQL with PlanetScale or NeonDB for Postgres.

~ Conclusion

That’s it! You just built a fully working URL shortener with:

  • App Router Route Handlers
  • Prisma ORM
  • SQLite DB
  • One-click redirect magic
  • Optional UI

You are now ready to work for google (homeless shelter)

Wanna add auth? Rate limiting? QR codes? Let me know!

Got questions or ideas?

Drop ‘em below.

You know what to do.

NOW!

~ Luan