Build URL Shortener Using Deno
Tags: deno, javascript, typescript
Published: 2020-06-30 15:51:56

<p>
Welcome again today we are creating a URL shortener application using Deno. As URL shortener simply create a unique id with respective full length URL you pass. So without further ado let’s get started …

First we need to create our <a href="https://medium.com/recoding/basic-routing-app-in-deno-using-oak-51caa06d2ee5">Oak application</a> along with Router so first we need to import our Oak module from its URL inside it pass application and router then we need to initiate our application and router class, after initiating we need to create some route to handle request on some GET and POST method. After create our router we need to pass our router inside our application as middleware and we can let listen our server any port we want. So our final code will look something like.
</p>

<pre>

// Importing Modules
import {Application, Router} from 'https://deno.land/x/oak/mod.ts';
// Initiating App and Router
const app = new Application();
const router = new Router();
// Handling Requests
router.get('/',async (ctx) => {
})
.post('/post',async (ctx)=>{
})
.get('/:shortId', async(ctx) => {
});
// Passing Router as middle ware inside the app
app.use(router.routes());
app.use(router.allowedMethods());
// Logging that server is running at PORT 8000
console.log('App is listening to port 8000');
await app.listen({port: 8000});

</pre>

<p>
After setting our application we can set view engine inside our Deno app. We are using EJS as our Views, so inside our view engine first we need to import viewEngine, engineFactory and adapterFactory. Then we need to set our engineFactory as EJS Engine and AdapterFactory as OakAdapter. So our code looks something like this …
</p>

<pre>

// Importing Modules
import {viewEngine,
engineFactory,
adapterFactory} from 'https://deno.land/x/view_engine/mod.ts';
// Setting up View Engine
const ejsEngine = engineFactory.getEjsEngine();
const oakAdapter = adapterFactory.getOakAdapter();
app.use(viewEngine(oakAdapter,ejsEngine));

</pre>

<p>
Now we can initialize our MongoClient, we are using Mongo local database while our can use online repository from mongo website you just need to pass the URI inside connectWithUri function and you database will work. Inside our application we are setting name of Database as “shortener” and collection as “url” you can change whatever you want.
</p>

<pre>

import {MongoClient} from 'https://deno.land/x/mongo@v0.8.0/mod.ts';
// Setting up mongo client
const client = new MongoClient();
// Connect Mongo Client to localhost
client.connectWithUri('mongodb://localhost:27017');
// Setting up name of Database
const db = client.database('shortener');
// Setting up name of collection
const urlCollection = db.collection('url');

</pre>

<p>

We can now setup our views we aren using Bootstrap for styling our application. And we are passing a form input type of text and button with type submit. Our form is method of POST and it has enctype of multipart/form-data. Then we can create our table which can fill when data pass through it. So create a new index.ejs file and pass the following code inside it

</p>

<pre>

&lt; !DOCTYPE html &gt;
&lt; html lang="en" &gt;
&lt; head &gt;
&lt; meta charset="UTF-8" &gt;
&lt; meta name="viewport" content="width=device-width, initial-scale=1.0" &gt;
&lt; title &gt; URL SHORTENER &lt; /title &gt;
&lt; !-- CSS only -- &gt;
&lt; link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/css/bootstrap.min.css" integrity="sha384-9aIt2nRpC12Uk9gS9baDl411NQApFmC26EwAOH8WgZl5MYYxFfc+NcPb1dKGj7Sk" crossorigin="anonymous" &gt;
&lt; /head &gt;
&lt; body &gt;
&lt; div class="container" &gt;
&lt; br / &gt;
&lt; h1 &gt; URL SHORTENER &lt; /h1 &gt;
&lt; form method="POST" enctype="multipart/form-data" action="/post" &gt;
&lt; div class="form-row" &gt;
&lt; div class="col-md-10 mb-3" &gt;
&lt; input type="text" class="form-control" name="url" placeholder="Enter your URL" required>
&lt; /div &gt;
&lt; div class="col-md-2 mb-3" &gt;
&lt; input class="btn btn-primary" type="button" value="Submit" &gt;
&lt; /div &gt;
&lt; /div &gt;
&lt; /form &gt;
&lt; table class="table" &gt;
&lt;thead &gt;
&lt; tr &gt;
&lt; th scope="col"&gt; URL &lt; /th &gt;
&lt; th scope="col"&gt; SHORT &lt; /th &gt;
&lt; th scope="col"&gt; CLICK &lt;/th &gt;
&lt; /tr &gt;
&lt; /thead &gt;
&lt; tbody &gt;
&lt; % for(url of data){ % &gt;
&lt; tr &gt;
&lt; td &gt; &lt;%= url.fullURL %>&lt; /td &gt;
&lt; td &gt;&lt;a href=<%= url.shortURL %>>&lt;%= url.shortURL % &gt; &lt;/a &gt; &lt;/td &gt;
&lt; td &gt;&lt;%= url.click %>&lt;/td &gt;
&lt; /tr &gt;
&lt; % } % &gt;
&lt; /tbody &gt;
&lt; /table &gt;
&lt; /div &gt;
&lt; !-- JS, Popper.js, and jQuery --&gt;
&lt; script src="https://code.jquery.com/jquery-3.5.1.slim.min.js" integrity="sha384-DfXdz2htPH0lsSSs5nCTpuj/zy4C+OGpamoFVy38MVBnE+IbbVYUew+OrCXaRkfj" crossorigin="anonymous">&lt; /script &gt;
&lt; script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous">&lt;/script>
&lt; script src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.0/js/bootstrap.min.js" integrity="sha384-OgVRvuATP1z7JjHLkuOU7Xw704+h835Lr+6QL9UvYjZE3Ipu6Tp75j7Bh/kR0JKI" crossorigin="anonymous"&gt; &lt;/script &gt;
&lt; /body &gt;
&lt; /html &gt;

</pre>

<p>
As our view has been ready not add our view inside our GET method which at root path as.
</p>

<pre>
...
router.get('/',async (ctx) => {
const allURL = await urlCollection.find({})
ctx.render('index.ejs',{data: allURL});
})
...
</pre>

<p>
Now we can focus on posting of our form first we need to initiate our “UUID” which help to generate unique ID, and “multiparser” which help to capture our form data. Inside our app.ts file pass following code
</p>

<pre>
//Importing UUID and Multiparser
import {multiParser} from 'https://raw.githubusercontent.com/deligenius/multiparser/master/mod.ts';
import ShortUniqueId from 'https://cdn.jsdelivr.net/npm/short-unique-id@latest/short_uuid/mod.ts';
...
// Initiating UUID
const UUID = new ShortUniqueId();
...
</pre>
<p>
Now we can handle our POST method first we need to capture our form data then we need to create a unique ID and then save our Full URL and Short ID inside our database. We can also track our click so pass initial value 0.
</p>
<pre>
...
.post('/post',async (ctx)=>{
const formData:any = await multiParser(ctx.request.serverRequest);
const urlObject = {
fullURL: formData.url,
shortURL: UUID(),
click: 0
}
await urlCollection.insertOne(urlObject);
ctx.response.redirect('/')
})
...
</pre>
<p>
Now after handling our post request we can also handle our get request as soon as we pass our url and short ID it will redirect to full url so our code looks something like this.
</p>
<pre>
...
.get('/:shortId', async(ctx) => {
const shortURLId = ctx.params.shortId;
const isURL = await urlCollection.findOne({shortURL: shortURLId});
if(isURL) {
ctx.response.status = 301;
await urlCollection.updateOne({_id: isURL._id},{$set: {click: isURL.click+1}})
ctx.response.redirect(`${isURL.fullURL}`);
}else{
ctx.response.status = 404;
ctx.response.body = "Sorry no page found";
}
})
...
</pre>
<p>
So now first start you application using Denon or Deno and open up your browser with “localhost:8000” and it will work. In case if you miss any code here is full code inside app.ts
</p>
<pre>
// Importing Modules
import {Application, Router} from 'https://deno.land/x/oak/mod.ts';
import {viewEngine,
engineFactory,
adapterFactory} from 'https://deno.land/x/view_engine/mod.ts';
import {multiParser} from 'https://raw.githubusercontent.com/deligenius/multiparser/master/mod.ts';
import {MongoClient} from 'https://deno.land/x/mongo@v0.7.0/mod.ts';
import ShortUniqueId from 'https://cdn.jsdelivr.net/npm/short-unique-id@latest/short_uuid/mod.ts';
// Initiating App and Router
const app = new Application();
const router = new Router();
// Setting up View Engine
const ejsEngine = engineFactory.getEjsEngine();
const oakAdapter = adapterFactory.getOakAdapter();
app.use(viewEngine(oakAdapter,ejsEngine));
// Setting up mongo client
const client = new MongoClient();
client.connectWithUri('mongodb://localhost:27017');
const db = client.database('shortener');
const urlCollection = db.collection('url');
// Initiating UUID
const UUID = new ShortUniqueId();
// Handling Requests
router.get('/',async (ctx) => {
const allURL = await urlCollection.find({})
ctx.render('index.ejs',{data: allURL});
})
.post('/post',async (ctx)=>{
const formData:any = await multiParser(ctx.request.serverRequest);
const urlObject = {
fullURL: formData.url,
shortURL: UUID(),
click: 0
}
await urlCollection.insertOne(urlObject);
ctx.response.redirect('/')
})
.get('/:shortId', async(ctx) => {
const shortURLId = ctx.params.shortId;
const isURL = await urlCollection.findOne({shortURL: shortURLId});
if(isURL) {
ctx.response.status = 301;
await urlCollection.updateOne({_id: isURL._id},{$set: {click: isURL.click+1}})
ctx.response.redirect(`${isURL.fullURL}`);
}else{
ctx.response.status = 404;
ctx.response.body = "Sorry no page found";
}
});
// Passing Router as middle ware inside the app
app.use(router.routes());
app.use(router.allowedMethods());
// Logging that server is running at PORT 8000
console.log('App is listening to port 8000');
await app.listen({port: 8000});
</pre>
{% github recoding-io/deno-todo-app %}
{% youtube sZCa2eHoLZ4 %}