DEV Community

TZGyn
TZGyn

Posted on • Updated on

How to make a URL Shortener from scratch

Making applications from scratch is my favorite way to learn how they work. This post will be discussing on how to make a URL Shortener from scratch.

url shortener example

URL shortener is extremely easy to make, great way to learn a language as a beginner in my opinion. The harder part is adding custom domains, analytics (such as getting user device information), grouping links and other features added on top of the URL shortener service. So here is how you can make one from scratch.

In this tutorial we will be using hono (nodejs), drizzle orm and postgres, but it can be done with any language and framework, check out my sveltekit/golang implementation kon.sh, source code in github.

Start by creating a new hono project

npm create hono@latest
Enter fullscreen mode Exit fullscreen mode

Then fill in the following information

create-hono@0.13.1
Ok to proceed? (y)
create-hono version 0.13.1
? Target directory url-shortener
? Which template do you want to use? nodejs
? Do you want to install project dependencies? yes
? Which package manager do you want to use? npm
Enter fullscreen mode Exit fullscreen mode

Make sure you have your postgres database ready and create a new database called url-shortener.

Under src/ folder there should be a index.ts file, this file contains the code to run your api server. Here we need to add 2 api routes.

  • create shortener
  • catch-all route to redirect incoming requests

src/index.ts

import { serve } from "@hono/node-server";
import { Hono } from "hono";

const app = new Hono();

app.post("/api/shortener", async (c) => {
    // create shortener route
    return c.text("Not yet implemented");
});

app.get("/:code", async (c) => {
    // redirect
    return c.text("Not yet implemented");
});

const port = 3000;
console.log(`Server is running on port ${port}`);

serve({
    fetch: app.fetch,
    port,
});
Enter fullscreen mode Exit fullscreen mode

Now we can install drizzle orm and initialize our database. First, install the required packages

npm i drizzle-orm postgres
npm i -D drizzle-kit
Enter fullscreen mode Exit fullscreen mode

Then we need to create a new db folder under the src folder, and add index.ts for initializing the db client, and schema.ts for the database schema.

src/db/schema.ts

import { pgTable, text, varchar } from "drizzle-orm/pg-core";

export const shortener = pgTable("shortener", {
    id: text("id").primaryKey(),
    link: varchar("link", { length: 255 }).notNull(),
    code: varchar("code", { length: 255 }).notNull().unique(),
});
Enter fullscreen mode Exit fullscreen mode

src/db/index.ts

import { drizzle } from "drizzle-orm/postgres-js";
import postgres from "postgres";
import * as schema from "./schema";

const queryClient = postgres(
    "postgres://postgres:password@127.0.0.1:5432/url-shortener"
);
export const db = drizzle(queryClient, { schema });
Enter fullscreen mode Exit fullscreen mode

Then create a drizzle.config.ts file at the root folder.

drizzle.config.ts

// drizzle.config.ts
import { defineConfig } from "drizzle-kit";

export default defineConfig({
    schema: "./src/db/schema.ts",
    out: "./drizzle",
    dialect: "postgresql",
    dbCredentials: {
        url: "postgres://postgres:password@127.0.0.1:5432/url-shortener",
    },
});
Enter fullscreen mode Exit fullscreen mode

Run npx drizzle-kit push to push the schema to the database.

npx drizzle-kit push
Enter fullscreen mode Exit fullscreen mode

After all the setup, we can finally work on the api, run npm run dev to start the server

npm run dev
Enter fullscreen mode Exit fullscreen mode

First make a random string generator. Create a new folder under src named utils, then create a index.ts file.

src/utils/index.ts

export function generateId(length: number) {
    let result = "";
    const characters =
        "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    const charactersLength = characters.length;
    for (let i = 0; i < length; i++) {
        result += characters.charAt(
            Math.floor(Math.random() * charactersLength)
        );
    }
    return result;
}
Enter fullscreen mode Exit fullscreen mode

Then update the create shortener route.

+ import { generateId } from "./utils";
+ import { shortener } from "./db/schema";
+ import { db } from "./db";

app.post("/api/shortener", async (c) => {
-       // create shortener route
+
+       const body = await c.req.json();
+
+       const link = body.link;
+
+       const code = generateId(6);
+
+       await db.insert(shortener).values({
+           id: generateId(8),
+           link,
+           code,
+       });
-       return c.text("Not yet implemented");
+       return c.json({ code });
});
Enter fullscreen mode Exit fullscreen mode

Then you can make a post request to the endpoint containing the link to generate a new shortener. Here's an example using Thunder Client in my VSCode:
thunder client example

Finally, update the redirect api.

+ import { eq } from "drizzle-orm";

app.get("/:code", async (c) => {
-       // redirect
+
+       const code = c.req.param("code");
+
+       const link = await db
+           .select()
+           .from(shortener)
+           .where(eq(shortener.code, code));
+
+       if (link.length === 0) {
+           return c.text("Invalid code");
+       }
-       return c.text("Not yet implemented");
+       return c.redirect(link[0].link);
});
Enter fullscreen mode Exit fullscreen mode

Then navigate to http://localhost:3000/{code} on your browser and you will be redirected to the original link.

That's it. URL shortener is a great way to start learning a new language, you get to use the language's popular backend framework and learn to communicate with database with the language.

There are so much more to explore, such as generating QR code for your shortener, recording redirects for analytics, custom domains and more. These are not covered in this tutorial.

Check out my github to see my works, all my projects are open source and free for anyone to learn and contribute.

I am also open for new ideas, although my current skill may not match the difficulty. Feel free to share them in the comments.

Top comments (9)

Collapse
 
jeffgca profile image
Jeff Griffiths

You should really post the whole codebase for an article like this, or at least mention all changes to files as you go. For example, it's useful for people to see the import statements in index.ts:

import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { db } from './db'
import { eq } from 'drizzle-orm'
import { generateId } from './utils'
import { shortener } from './db/schema'
Enter fullscreen mode Exit fullscreen mode
Collapse
 
tzgyn profile image
TZGyn

Yeah once I actually followed what I wrote I realized that too. Thanks for the tip. I experimented a bit with the diff code block, its easier to read but a bit tedious to remove the + signs.

Collapse
 
olanazih profile image
Olawale Omosekeji

This is great, thanks for sharing this. Creating a URL from scratch can be very stressful, especially trying to increase its features. I usually say if this is not for a new product, just use one of the common free ones around like y(dot)gy, it's complete with analytics, custom domains, and all

Collapse
 
christianpaez profile image
Christian Paez

Good job

Collapse
 
raulpenate profile image
The Eagle 🦅

Great article dawg! I was wondering about the cost perspective: How much would it cost if this were implemented on Vercel or AWS and used 10,000 times a day or a realistic number that you consider?

Collapse
 
tzgyn profile image
TZGyn

Not a serverless guy but judging by egress and computation cost it should be relatively cheap. I have heard big serverless bills but that is usually costed by ddos or infinite loop.

Collapse
 
ranpafin profile image
Francesco Panina

How do you handle collision?

Collapse
 
tzgyn profile image
TZGyn

Collision doesn't often occur unless the available slots are getting really small (or you use a very small generated string length), then you can +1 to the generated string length then you got 50x more slots. If you want to handle collision automatically you can +1 the length and write it to a file on collision error, then all the new shorteners will get the new length.

Collapse
 
heyeasley profile image
heyeasley 🍓🥭 • Edited

I like short link. So, I understand that I can build my own ? I used to see it in some applications but it is selled. Concretely, what are languages handled ?