URL Shortener using Flask

Web Development Image

    In this tutorial, we will build a URL shortener, a service that takes any URL and generates a shorter, more readable version like bit.ly. For this purpose, we’re going to use Flask. The application will allow users to enter a URL and an optional custom short id and generate a shorter version. 

    Step 1 — Setting Up Dependencies

    In this step, we will activate our Python virtual environment and install Flask using the pip package installer. To manage the virtual environment, we’re going to use Pipenv. If you haven’t installed it, you can use the below command to install it : 

    $ pip install pipenv

    After Pipenv is installed successfully, we can create and activate our virtual environment using the command :

    $ pipenv shell

    Once our virtual environment is created, we can install the required dependencies using the command :

    $ pipenv install Flask Flask-Migrate Flask-SQLAlchemy

    Flask-SQLAlchemy is a Flask extension that provides SQLAlchemy support. Flask-Migrate is another extension that supports SQLAlchemy database migrations via Alembic.
     

    Step 2 — Setting Up Project

    Instead of following the trivial way to set up a project, we’re going to follow a slightly more elaborate way that will give us a good base structure for scaling our application. You can learn more about this here.
    The application will exist in a package. In Python, a sub-directory that includes an __init__.py file is considered a package and can be imported. When we import a package, the __init__.py executes and defines what symbols the package exposes to the outside world.

    Let’s create a package called app, that will host the application.

    $ mkdir app

    The __init__.py for the app package is going to contain the following code:

    The script above simply creates the application object as an instance of class Flask imported from the flask package. The __name__ variable passed to the Flask class is a Python predefined variable, which is set to the name of the module in which it is used. Flask uses the location of the module passed here as a starting point when it needs to load associated resources such as template files. For all practical purposes, passing __name__ is almost always going to configure Flask correctly. The application then imports the routes module, which doesn't exist yet.

    One aspect that may seem confusing at first is that there are two entities named app. The app package is defined by the app directory and the __init__.py script and is referenced in the from app import routes statement. The app variable is defined as an instance of the class Flask in the __init__.py script, which makes it a member of the app package.

    Another peculiarity is that the routes module is imported at the bottom and not at the top of the script as it is always done. The bottom import is a workaround to circular imports, a common problem with Flask applications. We are going to see that the routes module needs to import the app variable defined in this script, so putting one of the reciprocal imports at the bottom avoids the error that results from the mutual references between these two files.

    So what goes in the routes module? The routes are the different URLs that the application implements. In Flask, handlers for the application routes are written as Python functions, called view functions. View functions are mapped to one or more route URLs so that Flask knows what logic to execute when a client requests a given URL.

    Here is our first view function, which we need to write in the new module named app/routes.py:

    This view function is actually pretty simple, it just returns a greeting as a string. The @app.getline above the function is decorator, a unique feature of the Python language. A decorator modifies the function that follows it. A common pattern with decorators is to use them to register functions as callbacks for certain events. In this case, the @app.getdecorator creates an association between the URL given as an argument and the function. In this example, the decorator associates the URL / to this function. This means that when a web browser requests this URL, Flask is going to invoke this function and pass the return value of it back to the browser as a response. If this does not make complete sense yet, it will in a little bit when we run this application.

    To complete the application, we need to have a Python script at the top level that defines the Flask application instance. Let’s call this script main.py, and define it as a single line that imports the application instance:

    Remember the two app entities? Here we can see both together in the same sentence. The Flask application instance is called app and is a member of the app package. The from app import app statement imports the app variable that is a member of the app package. 

    Now, since we are using an extensible way to set up our project, we’ll define SECRET_KEY(required for secured sessions) and DEBUG variables in a separate file called config.py at the top level. Add the following in the config.py file :

    The configuration settings are defined as class variables inside the Config class. As the application needs more configuration items, they can be added to this class, and later if we find that we need to have more than one configuration set, we can create subclasses of it.

    Also, we can now replace the code in __init__.py with the below code:

    In the above code, we just asked our Flask app to use the configurations as mentioned in the Config class.

    Our project has been set up successfully and we are ready to run it using the below command : 

    $ python main.py 


     

    Step 3 — Setting Up Database

    The first thing we are going to do is to update our Config class in config.py with the following code :

    Now we can add SQLAlchemy and Flask-Migrate in our project as :

    Now, to define our database tables, we’ll create a models.py within the app package. Inside that, we can write the following code :

    We first imported the db that we had initialized in the __init__.py. Then we created a ShortUrls class with few fields such as id(primary key), original_url(provided by the user), short_id(generated by us or provided by user), and created_at(timestamp).

    We can then make use of Flask-Migrate commands to migrate the database with the new tables. The commands to be used are : 

    flask db init — to initialize the database at the beginning(to be used only once)

    flask db migrate — to migrate the new changes to the database(to be used every time we make changes in the database tables)

    flask db upgrade — to upgrade our database with the new changes(to be used with migrate command)

    After we run the database initialization we will see a new folder called “migrations” in the project. This holds the setup necessary for Alembic to run migrations against the project. Inside of “migrations”, we will see that it has a folder called “versions”, which will contain the migration scripts as they are created.
     

    Step 4— Creating the Index Page for Shortening URLs

    In this step, we will create a Flask route for the index page, which will allow users to enter a URL that we then save into the database. This route will use the custom short id provided by the user or generate one on its own, construct the short URL, and then render it as a result.

    First, let’s move to routes.py file and create a Python function to generate short id.

    In order to generate a short id, we have used the choice method from Python’s random module. Also, we have used Python’s in-built string module for lower case alphabets, upper case alphabets, and digits.

    Now, we need to create a template for the index page that will be served by the index route. This template will have a simple form where user can input the original URL and custom short id(optional) and submit it. But we’ll not create index.html directly. We can make use of the Template Inheritance concept in Jinja2. So, let us create a templates directory within the app package and create a base.html file inside that. You can paste the HTML code into that file.

    <!doctype html>
    <html lang="en">
      <head>
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    
        <!-- Bootstrap CSS -->
        <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
    
        <title>{% block title %} {% endblock %}</title>
      </head>
      <body>
        <div class="container mt-3">
            {% for message in get_flashed_messages() %}
                <div class="alert alert-danger">{{ message }}</div>
            {% endfor %}
            {% block content %} {% endblock %}
        </div>
    
        <!-- Optional JavaScript -->
        <!-- jQuery first, then Popper.js, then Bootstrap JS -->
        <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
        <script src="https://cdn.jsdelivr.net/npm/@popperjs/[email protected]/dist/umd/popper.min.js" integrity="sha384-eMNCOe7tC1doHpGoWe/6oMVemdAVTMs2xqW4mwXrXsW0L84Iytr2wi5v2QjrP/xp" crossorigin="anonymous"></script>
        <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.min.js" integrity="sha384-cn7l7gDp0eyniUwwAZgrzD06kc/tftFf19TOAs2zVinnD/C7E91j9yyk5//jjpt/" crossorigin="anonymous"></script>
      </body>
    </html>

    Note that, for styling, we’re using Bootstrap here.

    Most of the code in the preceding block is standard HTML code required for Bootstrap. The <meta> tags provide information for the web browser, the <link> tag links the Bootstrap CSS files, and the <script> tags are links to JavaScript code that allows some additional Bootstrap features. Check out the Bootstrap documentation for more information.

    The <title>{% block title %} {% endblock %}</title> tag allows the inheriting templates to define a custom title. We use the for message in get_flashed_messages() loop to display the flashed messages (warnings, alerts, and so on). The {% block content %} {% endblock %} placeholder is where inheriting templates place the content so that all templates have access to this base template, which avoids repetition.

    Next, create the index.html file that will extend this base.html file:

    {% extends 'base.html' %}
    
    {% block content %}
        <h1 class="text-center mb-3">{% block title %} Welcome to Shorty {% endblock %}</h1>
        <div class="row">
            <div class="col-md-2"></div>
            <div class="col-md-8">
                <form method="post" action="{{url_for('index')}}">
                <div class="form-floating mb-3">
                    <input type="text" name="url" id="url"
                        placeholder="Enter looooooooooooong URL" class="form-control"
                        value="{{ request.form['url'] }}" autofocus></input>
                    <label for="url">URL</label>
                </div>
                <div class="form-floating mb-3">
                    <input type="text" name="custom_id" id="custom_id"
                        placeholder="Want to customise? (optional)" class="form-control"
                        value="{{ request.form['custom_id'] }}"></input>
                    <label for="custom_id">Custom Short ID</label>
                </div>
    
                <div class="form-group text-center">
                    <button type="submit" class="btn btn-lg btn-primary">Shorten</button>
                </div>
                </form>
    
                {% if short_url %}
                <hr>
                <span><a href="{{ short_url }}">{{ short_url }}</a></span>
                {% endif %}
            </div>
            <div class="col-md-2"></div>
        </div>
    {% endblock %}

    Here we extend base.html, define a title, and create a form with two inputs named url and custom_id. The url input will allow users to enter URLs to shorten. It has a value of request.form['url'], which stores data in cases of submission failure; that is if the user provides no URL. Similarly, custom_id input will allow users to enter custom short id. We then have a submit button.

    Then we check if the short_url variable has any value—this is true if the form submits and the short URL generates successfully. If the condition is true, we display the short URL under the form.

    Now we can re-write our index view function in routes.py as :

    from datetime import datetime
    from app.models import ShortUrls
    from app import app, db
    from flask import render_template, request, flash, redirect, url_for
    
    @app.route('/', methods=['GET', 'POST'])
    def index():
        if request.method == 'POST':
            url = request.form['url']
            short_id = request.form['custom_id']
    
            if short_id and ShortUrls.query.filter_by(short_id=short_id).first() is not None:
                flash('Please enter different custom id!')
                return redirect(url_for('index'))
    
            if not url:
                flash('The URL is required!')
                return redirect(url_for('index'))
    
            if not short_id:
                short_id = generate_short_id(8)
    
            new_link = ShortUrls(
                original_url=url, short_id=short_id, created_at=datetime.now())
            db.session.add(new_link)
            db.session.commit()
            short_url = request.host_url + short_id
    
            return render_template('index.html', short_url=short_url)
    
        return render_template('index.html')

    The index() function is a Flask view function, which is a function decorated using the special @app.route decorator. Its return value gets converted into an HTTP response that an HTTP client, such as a web browser, displays.

    Inside the index() view function, we accept both GET and POST requests by passing methods=['GET', 'POST'] to the app.route() decorator. 

    Then if the request is a GET request, it skips the if request.method == 'POST' condition until the last line. This is where we render a template called index.html, which will contain a form for users to enter a URL to shorten.

    If the request is a POST request, the if request.method == 'POST' condition is true, which means a user has submitted a URL. We store the URL in the url variable; if the user has submitted an empty form, you flash the message The URL is required! and redirect to the index page. If the user has entered custom_id, we store it in short_id, else we generate random short id using the function that we had created before.

    If the user has submitted a URL, we create a new_link with all the data such as original_url short_id and created_at. Then we commit the transaction.

    We then construct the short URL using request.host_url, which is an attribute that Flask’s request object provides to access the URL of the application’s host. This will be http://127.0.0.1:5000/ in a development environment and our_domain if we deploy our application. For example, the short_url variable will have a value like http://127.0.0.1:5000/asdf1gHJ, which is the short URL that will redirect users to the original URL stored in the database with the ID that matches the asdf1gHJ.

    Lastly, we render the index.html template passing the short_url variable to it.

    We can now run the server and test our view function.

    We have created a Flask application with a page that accepts URLs and generates shorter ones, but the URLs don’t do anything yet. In the next step, we’ll add a route that extracts the short_id from the short URL, finds the original URL, and redirects users to it.
     

    Step 5— Adding the Redirect Route

    In this step, we will add a new route that takes the short id the application generates and fetch the original URL. Finally, we will redirect users to the original URL.

    @app.route('/<short_id>')
    def redirect_url(short_id):
        link = ShortUrls.query.filter_by(short_id=short_id).first()
        if link:
            return redirect(link.original_url)
        else:
            flash('Invalid URL')
            return redirect(url_for('index'))

    This new route accepts a value short_id through the URL and passes it to the url_redirect() view function. For example, visiting http://127.0.0.1:5000/asdf1gHJ would pass the string 'asdf1gHJ' to the short_id parameter.

    Inside the view function, we fetch the link from the database using the short_id . If it is not None, the view function will redirect the user to the original_url associated with this short_id using the redirect() Flask helper function, else it will flash an error message to inform the user that the URL is invalid, and redirect them to the index page.

    Now we can again run the server and finally test the application. Watch the video here.


     

    Conclusion

    We have created a Flask application that allows users to enter a long URL and generate a shorter version. We can add more features to this application such as User Authentication, Shortened URLs Statistics, etc.

    Make sure you hit the like button if you liked this post.

    0 Comments

    To add a comment, please Signup or Login