DEV Community

Cover image for URL Shortener with Next.js Route Handlers + Prisma
Luan Dev
Luan Dev

Posted on • Edited on

3

URL Shortener with Next.js Route Handlers + Prisma

~ Index


~ 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
Enter fullscreen mode Exit fullscreen mode

Install dependencies

npm install prisma @prisma/client
npx prisma init
Enter fullscreen mode Exit fullscreen mode

Configure .env

DATABASE_URL="file:./dev.db"
NEXT_PUBLIC_BASE_URL="http://localhost:3000"
Enter fullscreen mode Exit fullscreen mode

~ 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())
}
Enter fullscreen mode Exit fullscreen mode

Create the database

npx prisma migrate dev --name init
Enter fullscreen mode Exit fullscreen mode

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 });
}
Enter fullscreen mode Exit fullscreen mode

~ 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);
}
Enter fullscreen mode Exit fullscreen mode

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 Shortener</h1>
      <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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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

ACI image

ACI.dev: The Only MCP Server Your AI Agents Need

ACI.dev’s open-source tool-use platform and Unified MCP Server turns 600+ functions into two simple MCP tools on one server—search and execute. Comes with multi-tenant auth and natural-language permission scopes. 100% open-source under Apache 2.0.

Star our GitHub!

Top comments (0)

👋 Kindness is contagious

Engage with a wealth of insights in this thoughtful article, cherished by the supportive DEV Community. Coders of every background are encouraged to bring their perspectives and bolster our collective wisdom.

A sincere “thank you” often brightens someone’s day—share yours in the comments below!

On DEV, the act of sharing knowledge eases our journey and forges stronger community ties. Found value in this? A quick thank-you to the author can make a world of difference.

Okay