URL Shortener service in Deno using Mongo DB

Mayank C
Tech Tonic

--

In this article, we’ll write a very simple URL shortener service in Deno using a Mongo database.

In other articles, we’ve seen web apps like:

Features

This simple URL shortening service supports the following features:

  • Provides an index page (or landing page) for users to give a long URL and get a shortened URL back
  • Provides a redirector service that handles the short URLs and redirects the caller to the long URL (basically it’s the reverse of shortening)

Technologies

The front end is developed using:

  • Vanilla JavaScript
  • Bootstrap CSS library
  • Sweet alert

The back end service is developed in Deno using:

  • Oak middleware framework module
  • Random string generator module

The back end storage uses:

  • Mongo DB

Application

The application code is present in the following GitHub repo:

The service contains folders specific to the type of storage: JSON file database, Mongo DB, Postgres, and MySQL. This article focuses on Mongo DB. The code for this service is present here.

Here is the walk-through of the complete code present inside the application.

cfg.json

This file contains two basic settings & one setting for Mongo access:

{
"serverPort": 80,
"shortenedDomain": "de.no",
"mongo": {
"user": "denoUser",
"password": "denoPwd",
"host": "127.0.0.1",
"port": "27017",
"db": "deno-db"
}
}

deps.ts

The dependencies are listed in this file:

export {
Application,
Context,
Router,
send,
} from "https://deno.land/x/oak/mod.ts";
export { brightRed } from "https://deno.land/std/fmt/colors.ts";
export { getRandomString } from "https://raw.githubusercontent.com/mayankchoubey/deno-random-id/main/mod.ts";
export { Collection, MongoClient } from "https://deno.land/x/mongo/mod.ts";

server.ts

This is the main module of the application. A quick sandbox check is done before starting the router and the HTTP server.

import * as router from "./src/router.ts";
import { printError } from "./src/utils.ts";
import cfg from "./cfg.json" assert { type: "json" };
async function checkAccess() {
if (
(await Deno.permissions.query({ name: "read", path: "./public" })).state !==
"granted"
) {
printError("Missing read permission to ./public");
Deno.exit(1);
}
if (
(await Deno.permissions.query({
name: "net",
host: `${cfg.shortenedDomain}:${cfg.serverPort}`,
})).state !== "granted"
) {
printError(
`Missing net permission to ${cfg.shortenedDomain}:${cfg.serverPort}`,
);
Deno.exit(1);
}
if (
(await Deno.permissions.query({
name: "net",
host: `${cfg.mongo.host}:${cfg.mongo.port}`,
})).state !== "granted"
) {
printError(`Missing net permission to ${cfg.mongo.host}:${cfg.mongo.port}`);
Deno.exit(1);
}
}
await checkAccess();
router.start();

The application runs on standard port 80 to seamlessly handle the browser traffic.

consts.ts

This file contains the interface & constants definitions:

export interface ShortenedData {
originalUrl: string;
targetUrl: string;
}
export interface ShortenedSchema {
_id: string;
originalUrl: string;
targetUrl: string;
}
export const COLLECTION_NAME = "shortened";export const URL_SIZE = 8;

router.ts

This file contains the code to create an Oak application, router, and starting the HTTP server. The only static file (index.html) is rendered directly. The router uses shortenController and redirectController. There are two primary routes:

  • POST /shorten: This is used to shorten a given URL. A target URL is returned.
  • GET/POST /:shortId: This is used to redirect user to the original URL.

The above two paths are opposite of each other.

import { Application, Context, Router } from "../deps.ts";
import cfg from "../cfg.json" assert { type: "json" };
import { shorten } from "./shortenController.ts";
import { redirect } from "./redirectController.ts";
export async function start() {
const router = new Router();
router
.get("/", (ctx) => ctx.response.redirect("./index.html"))
.get("/index.html", (ctx) => sendLandingPage(ctx))
.post("/shorten", (ctx) => shorten(ctx))
.get("/:shortId", (ctx) => redirect(ctx))
.post("/:shortId", (ctx) => redirect(ctx));
const app = new Application();
app.use(router.routes());
app.use(router.allowedMethods());
console.log(`Starting server on ${cfg.serverPort}`);
await app.listen(`${cfg.shortenedDomain}:${cfg.serverPort}`);
}
async function sendLandingPage(ctx: Context) {
ctx.response.body = await Deno.readFile("./public/index.html");
ctx.response.headers.set("Content-Type", "text/html");
}

shortenController.ts

This is the controller file for URL shortening functionality, whereby a given URL is shortened. It uses shortenService to shorten and save the record in Mongo DB.

import { shortenUrl } from "./shortenService.ts";export async function shorten(ctx: any) {
const urlToShorten = ctx.request.url.searchParams.get("urlToShorten");
if (!urlToShorten) {
ctx.response.status = 400;
ctx.response.body = { errMsg: "URL to shorten is missing" };
return;
}
try {
const shortenedUrl = await shortenUrl(urlToShorten);
ctx.response.status = 201;
ctx.response.body = { shortenedUrl };
} catch (e) {
ctx.response.status = 500;
ctx.response.body = { errMsg: e.message };
}
}

shortenService.ts

This is the business logic for the shorten service. The logic is simple:

  • Generate a short Id
  • Create a target URL using short Id
  • Save the record in Mongo DB
import { addRecord } from "./db.ts";
import { ShortenedData, URL_SIZE } from "./consts.ts";
import { getRandomString } from "../deps.ts";
import cfg from "../cfg.json" assert { type: "json" };
export async function shortenUrl(originalUrl: string): Promise<string> {
const shortId = await getRandomString(URL_SIZE);
const targetUrl = `http://${cfg.shortenedDomain}/${shortId}`;
const dbRec: ShortenedData = { originalUrl, targetUrl };
await addRecord(shortId, dbRec);
return targetUrl;
}

redirectController.ts

The redirectController is used to redirect a caller using a short URL to the original URL. It uses redirectService to get the Mongo DB record.

import { getShortenedUrl } from "./redirectService.ts";export async function redirect(ctx: any) {
try {
const shortenedUrl = await getShortenedUrl(ctx.params.shortId);
ctx.response.redirect(shortenedUrl);
} catch (e) {
ctx.response.status = 500;
if (e instanceof Deno.errors.NotFound) {
ctx.response.body = {
errMsg: "Provided URL is not a valid shortened URL",
};
}
}
}

redirectService.ts

This is the business logic for the redirector service.

import { getRecord } from "./db.ts";export async function getShortenedUrl(id: string) {
const rec = await getRecord(id);
if (!rec) {
throw new Deno.errors.NotFound();
}
return rec.originalUrl;
}

db.ts

This is the interface to the database, which is Mongo DB in this case. Connection to Mongo DB is attempted at startup. Any exceptions would cause application to quit immediately.

import { Collection, MongoClient } from "../deps.ts";
import { printError } from "./utils.ts";
import cfg from "../cfg.json" assert { type: "json" };
import { COLLECTION_NAME, ShortenedSchema } from "./consts.ts";
let dbConn: Collection<ShortenedSchema>;export async function addRecord(id: string, data: any) {
await dbConn.insertOne({
_id: id,
...data,
});
}
export async function getRecord(id: string) {
return await dbConn.findOne({ _id: id });
}
async function connectDatabase() {
const mongoPath =
`mongodb://${cfg.mongo.user}:${cfg.mongo.password}@${cfg.mongo.host}:${cfg.mongo.port}/${cfg.mongo.db}`;
try {
const client = new MongoClient();
await client.connect(mongoPath);
const db = client.database(cfg.mongo.db);
dbConn = db.collection<ShortenedSchema>(COLLECTION_NAME);
console.log("Connected to mongo database");
} catch (e) {
printError("Unable to connect to database: " + e.message);
Deno.exit(1);
}
}
connectDatabase();

utils.ts

This contains utility functions to:

  • print error message
import { brightRed } from "../deps.ts";export function printError(msg: string) {
console.log(`${brightRed("ERROR:")} ${msg}`);
}

Static files

There is a single static file used by this application:

  • public/index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
rel="stylesheet" crossorigin="anonymous">
<title>URL Shortener Service in Deno</title>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
async function doShorten() {
const urlToShorten = document.getElementById("urlToShorten").value;
if(!urlToShorten) {
swal.fire("Error", "URL is required to shorten it", "error");
return;
}
const q = new URLSearchParams({ urlToShorten });
const shortenEP = `${window.location.origin}/shorten?`+q;
const res = await fetch(shortenEP, {
method: 'POST'
});
const resJson = await res.json();
if(res.status === 201) {
Swal.fire("URL Shortened successfully!",
`${resJson.shortenedUrl}`,
"success"
).then(() => {
window.location.reload();
});
} else {
Swal.fire({
icon: "error",
title: "URL Shortening failed!",
text: resJson.errMsg
});
}
}
</script>
</head>
<body>
<section class="vh-100">
<div class="container py-5 h-100">
<div class="row d-flex justify-content-center align-items-center h-100">
<div class="col-12 col-md-8 col-lg-6 col-xl-5">
<div class="card shadow-2-strong" style="border-radius: 1rem;">
<div class="card-body p-5 text-center">
<h1>URL Shortener Service</h1>
<h5 class="mb-4">Written in Deno with Mongo</h5>
<form class="form-inline">
<div class="form-group mb-2">
<input type="text" class="form-control text-center" id="urlToShorten" placeholder="Enter URL to shorten">
</div>
</form>
<button class="btn btn-primary mb-2" onclick="doShorten()">Shorten it!</button>
</div>
</div>
</div>
</div>
</div>
</section>
</body>
</html>

Testing

The application server can be started by going to appopriate type in the base repo and running the runApp script.

Pre-requisite

As dummy hostname is used instead of localhost (de.no), we need to set it up in /etc/hosts file:

> cat /etc/hosts | grep -i "de\.no"
127.0.0.1 de.no

Starting backend

> git clone https://github.com/mayankchoubey/deno-url-shortener-service.git> cd deno-url-shortener-service> cd app-mongo-db> cat runApp 
deno run --no-prompt --allow-read=./ --allow-net=de.no:80,:27017 --watch server.ts
> sudo ./runApp
Starting server on 80
Connected to mongo database

As the application runs on standard port 80, the startup script may need sudo (depending on system configuration).

We can also confirm that Mongo collection is totally empty:

> db.shortened.count();
0
>

Open the service in the browser

Use URL http://de.no to open the service:

It presents a very simple interface to shorten URLs.

Enter URL to shorten

Enter any URL that needs to be shortened. We’ll use oakserver GitHub repo:

Once shortened, the short URL will be displayed in a popup.

We can quickly verify the database:

> db.shortened.count();
1
> db.shortened.find().pretty();
{
"_id" : "36ba9fc0",
"originalUrl" : "https://github.com/oakserver/oak",
"targetUrl" : "http://de.no/36ba9fc0"
}
>

Use short URLs

The short URls can be opened in browser:

That’s all about the simple URL shortening service in Deno using Mongo DB.

In other articles, we’ve seen web apps like:

--

--