GeoIP Location on World Map
Home Django Build an IP Geolocation SaaS with Python/Django - Part 1

Build an IP Geolocation SaaS with Python/Django - Part 1

13 June, 2024

Have you ever wondered how does Netflix know where are you accessing their service from? Companies like Netflix adjust their offering in order to comply to local legislation and for providing geographically relevant experiences. You also probably have experienced how an online service changes its language and currency depending where you access it from. All this information comes from the source IP address of the client.

I was really amazed as a kid when I first tried out one of these services and they pinpointed where I was on a map with impressive accuracy.

Here are a few paid services doing just that:
  • ipinfo.io
  • ipstack.com
  • ipdata.co

The geo-location data is provided by a company called MaxMind, which offers two types of databases: GeoLite2 (Free) and GeoIP2 (Paid). For keeping things simple, we are going to use GeoLite2 but keep in mind that the databases only differ in the quantity of information they offer. They way they work is exactly the same.

For this series we are going to leverage the Python/Django stack and the MaxMind databases.

Here's what you need to know before getting started:

Frequently Asked Questions

You should have a basic understanding of Python and some familiarity with web development in general. You'll also need a development environment where you can run Python and Django. We'll walk you through installing Django and the MaxMind GeoIP database, so don't worry if you haven't used them before!

GeoIP helps with identifying the geographical location of an internet-connected device from its IP address. It's super useful for personalizing user experiences or for enhancing security.

MaxMind's GeoIP databases are the most popular choice when it comes to IP geolocation. They are very accurate and are used by many companies for delivering better and more secure experiences to their customers.

The Python language is known for its simplicity and readability, and is currently one of the most popular programming languages used for building web applications. Django, a Python web framework, comes packed with a lot of built-in features to help you build webapps with fewer lines of code. Django is also one of our favorite Python libraries on this blog.

In this tutorial we are only going to make use of MaxMind's free GeoLite2 database. If you want to enhance your project with more detailed GeoIP information you can use MaxMind's GeoIP2 database, which is a paid service so make sure you consult all the fees involved.

Go right ahead! Just make sure to read through MaxMind's terms of service and the licenses of all the libraries you use.

Let's start things off by installing Django and initializing the project:

pip install Django

Let's name the Django project ipdb as an acronym for IP DataBase.

django-admin startproject ipdb
In most of our projects here we are going to use the SQLite3 database for storing our data. The main reason is that it drastically reduces the complexity of the development processes and we're all about that here. Recently many developers started admitting using SQLite3 as their defacto database even for large projects. Here is a more detailed list of reasons for using SQLite:
  1. Super easy to setup: SQLite3 doesn't require a separate server to run, making it incredibly easy to install and use. The entire database is contained in a single file.
  2. Zero Configuration: There are 0 knobs to turn which reduces the complexity of deployment and administration.
  3. Makes Prototyping easy-peasy: Let's you focus on development without worrying about database management.
  4. Portable & Cross Platform: Copy/Paste the database file between environments and on any supported OS.

We are now going to run the Django migrate command. What this does is it creates the SQLite3 database file and will apply the Django migrations on that database.

python manage.py migrate

You should see an output similar to this one:

ipdb % python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, sessions
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying admin.0001_initial... OK
  Applying admin.0002_logentry_remove_auto_add... OK
  Applying admin.0003_logentry_add_action_flag_choices... OK
  Applying contenttypes.0002_remove_content_type_name... OK
  Applying auth.0002_alter_permission_name_max_length... OK
  Applying auth.0003_alter_user_email_max_length... OK
  Applying auth.0004_alter_user_username_opts... OK
  Applying auth.0005_alter_user_last_login_null... OK
  Applying auth.0006_require_contenttypes_0002... OK
  Applying auth.0007_alter_validators_add_error_messages... OK
  Applying auth.0008_alter_user_username_max_length... OK
  Applying auth.0009_alter_user_last_name_max_length... OK
  Applying auth.0010_alter_group_name_max_length... OK
  Applying auth.0011_update_proxy_permissions... OK
  Applying auth.0012_alter_user_first_name_max_length... OK
  Applying sessions.0001_initial... OK

Database Migrations

Django migrations are essentially version control for your database. They help you manage changes to your database schema, so it aligns with your Django models.

Migrations typically are automatically (but you can also write your onw migrations) generated Python files stored in a migrations folder within each Django app. The main job of the migrations is to make sure the Django models you define are in sync with the schema of the database.

Creating a Django Application

Let's get into the meat of it and create a Django application. Django projects are structured as a set of applications that should function independently of each other as much as possible.

python manage.py startapp ipdata

Notice how Django created a new package ipdata that contains several Python files like models.py and views.py. Creating the application is not enough, we also need to install it in the ipdb/settings.py file. This is done by adding ipdata to the INSTALLED_APPS list:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'ipdata',
]

We now need to create the actual Django view that serves the Geo IP data. For now, let's just mock it up, and we're going to come back to it later. Drop this code into ipdb/ipdata/views.py:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from django.http import JsonResponse


# Create your views here.
def ip_data(request):
    try:
        ip = request.GET['ip']
    except KeyError:
        return JsonResponse({'error': 'IP address not provided'}, status=400)
    return JsonResponse({'ip': ip})

Let's include that view in the app's url pattern list in ipdb/ipdata/urls.py:

1
2
3
4
5
6
7
8
from django.urls import path

from . import views


urlpatterns = [
    path('ip_data', views.ip_data, name="ip_data"),
]

In order for the API URL to be accessible we need to hook up the ipdb/ipdata/urls.py URL patterns in the to the global URL schema (located in ipdb/ipdb/urls.py) like this:

1
2
3
4
5
6
7
8
from django.contrib import admin
from django.urls import path, include
from ipdata import urls as ipdata_urls

urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/v1/', include(ipdata_urls)),
]

Now, if you start Django's development server (python manage.py runserver) you should be able to visit http://localhost:8000/api/v1/ip_data?ip=12.13.14.15 in your favorite browser.

Serving GeoIP data

Let's start things up by installing some required libraries:

pip install geoip2 pytz

The actual GeoIP data does not come with the library, you need to download it from the MaxMind website. You will have to create an account. Download files from GeoLite2 Free Geolocation Data

You will find several download options, various files and formats (.mmdb and .csv). Here are the files you need to download:

  1. GeoLite2-ASN.mmdb - Autonomous System Number data in MaxMind format
  2. GeoLite2-Country.mmdb - Country level data in MaxMind format
  3. GeoLite2-City.mmdb - City level data in MaxMind format

The country level data is a bit redundant since all the information is present in the city level database.

Let's now see how to read from these databases. Let's acknowledge first that Django already provides some wrappers over the MaxMind API but we are not going to use it since it doesn't provide a wrapper for the ASN data, which is essential for our API. Here's the GeoIP2 Django Documentation.

Next step is to initialize the MaxMind readers and we are going to do that in the ipdb/ipdata/models.py file. We don't want the readers to be initialized every API call as it is a costly operation. That's why we don't init them inside the view.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import geoip2.database

from django.db import models
from django.conf import settings

GEOIP_ASN_READER = geoip2.database.Reader(
            settings.GEOIP_PATH / "GeoLite2-ASN.mmdb")
GEOIP_CITY_READER = geoip2.database.Reader(
            settings.GEOIP_PATH / "GeoLite2-City.mmdb")
GEOIP_COUNTRY_READER = geoip2.database.Reader(
            settings.GEOIP_PATH / "GeoLite2-Country.mmdb")

Now let's write the view that ties everything together. Notice how we are making use of the various MaxMind APIs for gathering info about our IPs and how we are using the pytz API to get information about the timezone.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import pytz
import datetime
from pytz.exceptions import UnknownTimeZoneError

from django.http import JsonResponse

from .models import GEOIP_ASN_READER, GEOIP_CITY_READER


# Create your views here.
def ip_data(request):
    try:
        ip = request.GET['ip']
    except KeyError:
        return JsonResponse(
            {'error': 'IP address not provided'},
            status=400
        )

    city_response = GEOIP_CITY_READER.city(ip)
    asn_response = GEOIP_ASN_READER.asn(ip)

    time_zone = city_response.location.time_zone
    try:
        tz = pytz.timezone(time_zone)
        current_time = datetime.datetime.now(tz)
    except UnknownTimeZoneError:
        tz, current_time = None, None

    ip_data = {
        'ip': ip,
        'network': str(city_response.traits.network),
        'country': {
            'name': city_response.country.name,
            'iso_code': city_response.country.iso_code,
            'is_eu': city_response.country.is_in_european_union,
        },
        'region': {
            'name': city_response.subdivisions.most_specific.name,
            'iso_code': city_response.subdivisions.most_specific.iso_code,
        },
        'coordinates': {
            'latitude': city_response.location.latitude,
            'longitude': city_response.location.longitude,
        },
        'city': {
            'name': city_response.city.name,
            'postal_code': city_response.postal.code
        },
        'asn': {
            'number': asn_response.autonomous_system_number,
            'organization': asn_response.autonomous_system_organization,
        },
        'timezone': {
            'name': time_zone,
            'current_time': (
                current_time.strftime("%Y-%m-%dT%H:%M:%S")
                if current_time is not None else None
            ),
            'is_dst': (
                bool(current_time.dst())
                if current_time is not None else None
            ),
            'offset': (
                current_time.strftime('%z')
                if current_time is not None else None
            ),
        }
    }

    return JsonResponse(ip_data, json_dumps_params={'indent': 2})

Let's now see it in action:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
% curl -X GET "http://localhost:8000/api/v1/ip_data?ip=12.13.14.15"
{
    "ip": "12.13.14.15",
    "network": "12.13.12.0/22",
    "country": {
        "name": "United States",
        "iso_code": "US",
        "is_eu": false
    },
    "region": {
        "name": "Arkansas",
        "iso_code": "AR"
    },
    "coordinates": {
        "latitude": 34.7456,
        "longitude": -92.3419
    },
    "city": {
        "name": "Little Rock",
        "postal_code": "72205"
    },
    "asn": {
        "number": 7018,
        "organization": "ATT-INTERNET4"
    },
    "timezone": {
        "name": "America/Chicago",
        "current_time": "2023-08-30T12:12:49",
        "is_dst": true,
        "offset": "-0500"
    }
}

Here's how that looks in the browser:

Call the GeoIP API in the browser

Congrats, you now have a working API that you can use to get information about IPs. Make sure to save your progress and meet you in the next sections where we are going to build upon our API.

Here are a few things we are going to cover next:

  • Authentication - Learn how to authenticate users via email/password via API keys
  • Homepage with map - Create a landing page showing a map with the users current location and IP data
  • Profiling and Optimization - Let's make things run faster
  • UnitTesting - Let's make sure things run properly
  • Rate Limiting - Don't let people abuse our APIs

See you in the next one!