The JS runtimes
Published in

The JS runtimes

URL Shortener service in Deno using JSON file database

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

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

Features

This simple URL shortening service supports the following features:

  • 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:

  • Bootstrap CSS library
  • Sweet alert

The back end is developed in Deno using:

  • Random string generator module

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 JSON file data. 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:

{
"serverPort": 80,
"shortenedDomain": "de.no"
}

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";

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: "read", path: "./localdb" }))
.state !== "granted"
) {
printError("Missing read permission to ./localdb");
Deno.exit(1);
}
if (
(await Deno.permissions.query({ name: "write", path: "./localdb" }))
.state !== "granted"
) {
printError("Missing write permission to ./localdb");
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);
}
}
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 const DB_PATH = "./localdb/shortened.json";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:

  • 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 JSON file 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:

  • Create a target URL using short Id
  • Save the record in database
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) {
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 database 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 a simple JSON file in this case.

import { DB_PATH } from "./consts.ts";export async function addRecord(id: string, data: any) {
const file = JSON.parse(await Deno.readTextFile(DB_PATH));
file[id] = data;
await Deno.writeTextFile(DB_PATH, JSON.stringify(file));
}
export async function getRecord(id: string) {
const file = JSON.parse(await Deno.readTextFile(DB_PATH));
return file[id];
}
async function checkDatabase() {
try {
await Deno.stat(DB_PATH);
} catch (e) {
await Deno.writeTextFile(DB_PATH, JSON.stringify({}));
}
}
checkDatabase();

utils.ts

This contains utility functions to:

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:

<!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'
});
alert(res);
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</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-json-db> cat runApp 
deno run --no-prompt --allow-read=./ --allow-write=./localdb --allow-net=de.no:80 --watch server.ts
> sudo ./runApp
Starting server on 80

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

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.

Use short URLs

The short URls can be opened in browser:

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

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

This story is a part of the exclusive medium publication on Deno: Deno World.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store