Python for Network Engineers

Implement a Url Shortener Service in Flask

by: George El., January 2021, Reading time: 5 minutes

In this post I will describe how to implement a url shortener service using flask. There are two main ways to implement a url shortener service. The first one is to take the URL and apply the md5 hash algorigthm. Then convert this 128bit number to base62 or base64. The problem with this approach is that the base62 results in a 21 byte string which is long. If you want, you can take a part of this string, but this can lead to collisions. The other apprach is to take the generated id of the database, each time a url is submitted and convert it to base62 or base64. Because this id is unique, there won’t be any collisions. We take the latter approach here.

the main page looks like this:

url-shortener-app

If we click submit we will get back the short url url-shortener-app

If we click the short url or paste it in our browser we will be redirected to the actual site url-shortener-app

If we try to submit a url that already exists in the database we will get a page that says already exists. e.g. www.imdb.com already exists: http://to.to/c

Finally all the urls in the database can be retrieved from /urls url-shortener-app

Our database will contain only one table. We don’t need to store the short url because it will be generated dynamically from the long url. Also provided the short url we can get the index and return the long url by querying the id of the database.

models.py

from . import db

class URL(db.Model):
    """
    Data model for URLs
    """
    __tablename__ = "URLs"
    id = db.Column(db.Integer, primary_key=True)
    url_long = db.Column(db.String(80), index=True, unique=True, nullable=False)
    created = db.Column(db.DateTime, index=False, unique=False, nullable=False)

    def __repr__(self):
        return "<URL {}>".format(self.url_long)

The routes

The app will have 2 main routes.

/

will provide a form for the user to submit the long url,

/<urlshort> 

will take as argument the short url and redirect the user to the long url page. We have compressed the two routes into one function using two decorators

routes.py

from flask import current_app as app
from .forms import UrlForm
from .models import URL, db
from .utilities import convert_dec_to_base62, convert_base62_to_dec
from datetime import datetime as dt
from flask import current_app as app
from flask import make_response, redirect, render_template

@app.route("/", methods=["GET", "POST"], defaults={'urlshort':None})
@app.route("/<urlshort>",methods=["GET", "POST"])
def home(urlshort):
    if urlshort:
        url_id = convert_base62_to_dec(urlshort)
        url = URL.query.get(url_id)
        if url:
            url_redirect = 'https://'+url.url_long
            print(url_redirect)
            return redirect(url_redirect,code=301)
        else:
            return make_response(f"{urlshort} doesn't exist")

    form = UrlForm()
    if form.validate_on_submit():
        url_long = form.website.data
        existing_url = URL.query.filter(
            URL.url_long == url_long 
        ).first()
        if existing_url:
             return make_response(f"{url_long} already exists: http://to.to/{convert_dec_to_base62(existing_url.id)}")
        new_url = URL(
            url_long = url_long,
            created = dt.now(), 
        )
        db.session.add(new_url)  
        db.session.commit()  
        url_short = convert_dec_to_base62(new_url.id)
        return render_template("success.html", url_short=url_short, url_long=url_long)
    return render_template("index.html", form=form)

We also have another route that displays all the urls. This is not needed. It is just for showing the generated URLs

/urls
@app.route("/urls",methods=["GET"])
def show_urls():
    urls  = URL.query.all()
    return render_template("urls.html", urls = urls, func=convert_dec_to_base62)

we also use some helper functions to convert a decimal number to base62 and vice versa in

utilities.py


import hashlib
import string
import sys

s='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
BASE = len(s)

def convert_dec_to_base62(x):
    """ 
    converts decimal to base62
    """
    result = []
    while x:
        x, r = divmod(x, BASE) 
        result.append(s[r])
    return ''.join(result)[::-1][:8]

def convert_base62_to_dec(b62):
    """ 
    converts base62 number to decimal 
    """
    ret = 0
    for i in range(len(b62)-1,-1,-1): #apo 61 os 0
        ret = ret + s.index(b62[i]) * (62**(len(b62)-i-1))
    return ret

our forms are simple

forms.py

from flask_wtf import FlaskForm
from wtforms import (
    StringField,
    SubmitField,
)

class UrlForm(FlaskForm):
    """
    URL form
    """
    website = StringField("Website")
    submit = SubmitField("Submit")

and our templates starting with

index.html

{% extends 'layout.html' %}
{% import "bootstrap/wtf.html" as wtf %}

{% block styles %}
    <link rel="stylesheet" href="{{ url_for('static', filename='css/home.css') }}" rel="stylesheet" type="text/css">
{% endblock %}

{% block content %}
<h1 class="">URL Shortener Service</h1>

<form method="POST" action="/">
    {{ form.csrf_token }}
  <div class="form-group">
    {{ wtf.form_field(form.website, label='', class='form-control', placeholder='enter your website here without https://...') }}
    {% if form.website.errors %}
      <ul class="errors">
        {% for error in form.website.errors %}
          <li>{{ error }}</li>
        {% endfor %}
      </ul>
    {% endif %}
  </div>
    <button type="submit" class="btn btn-primary my-2">Submit</button>
  </form>
{% endblock %}

layout.html

<!DOCTYPE html>

<html lang="en">
<head>
  <meta charset="utf-8"/>
  <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
  <title>{{ title }}</title>
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
  <link rel="shortcut icon" href="{{ url_for('static', filename='dist/img/favicon.png') }}" type="image/x-icon"/>
  <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}" rel="stylesheet" type="text/css">
</head>

<body>
<div class="container">
    <div class="row justify-content-center">
        <div class="column col-4">
        {% block content %}{% endblock %}
        </div>
    </div>
</div>
</body>
</html>

success.html

{% extends 'layout.html' %}

{% block content %}
<div class="success-wrapper">
  <h1>SUCCESS!</h1>
  <p>Your short url is <a href="http://to.to:5000/{{ url_short }}">http://to.to/{{ url_short }}</a></p>
  <p>Your long url is {{ url_long }}</p>
</div>
{% endblock %}

urls.html

{% extends 'layout.html' %}

{% block content %}
<h1>URL Shortener Service</h1>
    <ul>
    {% for url in urls %}
        {% with x = url.id %}
        <li>{{ url.url_long }} <a href="http://to.to:5000/{{ func(x) }}">http://to.to/{{ func(x) }}</a></li>
        {% endwith %}    
    {% endfor %}
    </ul>

{% endblock %}

init.py

"""Initialize Flask app."""
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_bootstrap import Bootstrap

db = SQLAlchemy()

def create_app():
    """Construct the core application."""
    app = Flask(__name__, instance_relative_config=False)
    app.config.from_object("config.Config")
    Bootstrap(app)
    db.init_app(app)

    with app.app_context():
        from . import routes  
        db.create_all()  
        return app

wsgi.py

"""Application entry point."""
from flask_short_url import create_app

app = create_app()

if __name__ == "__main__":
    app.run(host="0.0.0.0", debug=True)

One last thing, in your hosts file you need to put

127.0.0.1 to.to

Or whatever other short domain you want. In practice this will be the domain you will buy.

Of course this is running on a development server on port 5000. if you want to make it production ready, you have to install uwsgi and nginx, use a production db like postgres and also issue a certificate to make it https.

comments powered by Disqus