Day 7: Shorturl service

Today, we will develop a url shortening service like bit.ly or something

imports

import jester, asyncdispatch, htmlgen, json, os, strutils, strformat, db_sqlite
  • jester: is sinatra like framework

  • asyncdispatch: for async/await instructions

  • htmlgen: to generate html pages

  • json: to parse json string into nim structures and dump json structures to strings

  • db_sqlite: to work on sqlite databse behind our application

Database connection

# hostname can be something configurable "http://ni.m:5000" let hostname = "localhost:5000" var theDb : DbConn
  • hostname is the basepath for our site to access it, and can be configurable using /etc/hosts file or using even reverse proxy like caddy, or in real world case you will have a dns record for your site.

  • theDb is the connection object to work with sqlite database.

if not fileExists("/tmp/mytest.db"): theDb = open("/tmp/mytest.db", nil, nil, nil) theDb.exec(sql("""create table urls ( id INTEGER PRIMARY KEY, url VARCHAR(255) NOT NULL )""" )) else: theDb = open("/tmp/mytest.db", nil, nil, nil)
  • We check if the database file doesn't exist /tmp/mytest.db we create a urls table otherwise we just get the connection and do nothing

Jester and http endpoints

routes:
  • jester defines a DSL to work on routes
METHOD ROUTE_PATH: ##codeblock
  • METHOD can be get post or any http verb

  • ROUTE_PATH is the path accessed on the server for instance /users, /user/52, here 52 is a query parameter when route is defined like this/user/@id

HOME page

Here we handle GET requests on /home path on our server:

get "/home": var htmlout = """ <html> <title>NIM SHORT</title> <head> <script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script> <script> function postData(url, data) { // Default options are marked with * return fetch(url, { body: JSON.stringify(data), // must match 'Content-Type' header cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached credentials: 'same-origin', // include, same-origin, *omit headers: { 'user-agent': 'Mozilla/4.0 MDN Example', 'content-type': 'application/json' }, method: 'POST', // *GET, POST, PUT, DELETE, etc. mode: 'cors', // no-cors, cors, *same-origin redirect: 'follow', // manual, *follow, error referrer: 'no-referrer', // *client, no-referrer }) .then(resp => resp.json()) } $(document).ready(function() { $('#btnsubmit').on('click', function(e){ e.preventDefault(); postData('/shorten', {url: $("#url").val()}) .then( data => { let id = data["id"] $("#output").html(`<a href="%%hostname/${id}">Shortlink: ${id}</a>`); }); }); }); </script> </head> <body> <div> <form> <label>URL</label> <input type="url" name="url" id="url" /> <button id="btnsubmit" type="button">SHORT!</button </form> </div> <div id="output"> </div> </body> </html> """ htmlout = htmlout.replace("%%hostname", hostname) resp htmlout
  • Include jquery framework

  • Create a form with in div tag with 1 textinput to allow user to enter a url

  • override form submission to do an ajax request

  • on the button shorturl click event we send a post request to /shorten endpoint in the background using fetch api and whenever we get a result we parse the json data and extract the id from it and put the new url in the output div

  • resp to return a response to the user and it can return a http status too

Shorten endpoint

post "/shorten": let url = parseJson(request.body).getOrDefault("url").getStr() if not url.isNilOrEmpty(): var id = theDb.getValue(sql"SELECT id FROM urls WHERE url=?", url) if id.isNilOrEmpty(): id = $theDb.tryInsertId(sql"INSERT INTO urls (url) VALUES (?)", url) var jsonResp = $(%*{"id": id}) resp Http200, jsonResp else: resp Http400, "please specify url in the posted data."

Here we handle POST requests on /shorten endpoint

  • get the url from parsed json post data. please note that POST data is available under request.body explained in the previous section

  • if url is passed we try to check if it's there in our urls table, if it's there we return it, otherwise we insert it in the table.

  • if the url isn't passed we return a badrequest 400 status code.

  • parseJson: loads json from a string and you can get value using getOrDefault and getStr to get string value, there's getBool, and so on.

  • getValue to get the id from the result of the select statement returns the first column from the first row in the result set

  • tryInsertId executes insert statement and returns the id of the new row

  • after successfull insertion we would like to return json serialized string to the user $(%*{"id": id})

  • %* is a macro to convert nim struct into json node and to convert it to string we wrap $ around it

Shorturls redirect

get "/@Id": let url = theDb.getValue(sql"SELECT url FROM urls WHERE id=?", @"Id") if url.isNilOrEmpty(): resp Http404, "Don't know that url" else: redirect url
  • Here we fetch whatever path @Id the user trying to access except for /home and /shorten and we try to get the long url for that path

  • If the path is resolved to a url we redirect the user to to or we show an error message

  • @"Id" gets the value of @Id query parameter : notice the @ position in both situation

RUN

runForever()

start jester webserver

Code is available here https://gist.github.com/xmonader/d41a5c9f917eadb90d3025e7b7e748dd