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 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
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
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,
});
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
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(),
});
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 });
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",
},
});
Run npx drizzle-kit push
to push the schema to the database.
npx drizzle-kit push
After all the setup, we can finally work on the api, run npm run dev
to start the server
npm run dev
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;
}
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 });
});
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:
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);
});
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 (10)
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:
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.
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
Good job
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?
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.
How do you handle collision?
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.
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 ?