Your Own GeoIP SaaS Server



This content originally appeared on DEV Community and was authored by Saad Alkentar

In modern web development, there’s often a need to determine a user’s geographical location without using the browser’s Geolocation API, which requires explicit user permission. A common alternative is using GeoIP, which maps a user’s IP address to a physical location.

While this is a powerful and reliable method, it’s important to be aware of the costs. Many GeoIP service providers charge based on the number of API requests, and for high-traffic applications, these costs can escalate significantly.

Here’s a breakdown of some popular GeoIP providers and their approximate monthly costs for a bundle of one million requests:

The Idea

In this tutorial, we’re going to build our own GeoIP server. I’ll walk you through setting up the core functionality of a GeoIP SaaS application using Django, the GeoIP2 library, and the DB-IP database. While this tutorial will focus on the main GeoIP feature, a separate tutorial will cover the basic setup of a SaaS server.
Let’s dive in!

Django project setup

Let’s start by creating the Django project and the apps for accounts, admin, and main functionality app

# install Django lib and extensions
pip install Django
pip install django-filter
pip install djangorestframework
pip install djangorestframework_simplejwt
pip install pillow

pip install geoip2

# create the project
django-admin startproject saas
cd saas
python manage.py startapp app_account
python manage.py startapp app_admin
python manage.py startapp app_main

The next step is to setup GeoIP2 library in the project settings

...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'corsheaders',
    'rest_framework', # for restful api
    'django_filters',
    'app_account', # the apps
    'app_admin',
    'app_main',
]
...
GEOIP_PATH =os.path.join(BASE_DIR, 'geoip') # the location of ip db

saas/settings.py

Let’s create a folder in the main app path with the name ‘geoip’ to store the IP databases in the next step.

PS. I highly recommend using the account management app from previous tutorials to save time and effort later on.

DP-IP datasets

Now that we have our project ready, we can download IP databases from DP-IP. It has multiple free “IP Geolocation Databases” that update monthly. Let’s start by downloading the database we want to use from

https://db-ip.com/db/lite.php

We will use IP to city db, after agreeing with the licensing terms, make sure to download the MMDB version of the database. It was about 60MB at the time of writing this tutorial.

Extract the downloaded MMDB file, rename it to GeoLite2-City.mmdb, and make sure to put it in the geoip folder in the project folder

GeoIP implementation testing

Let’s check if it works. Firstly, we need to migrate the dataset and make sure the project works properly

python manage.py makemigrations
python manage.py migrate
python manage.py runserver 0.0.0.0:8555

This should run the project on port 8555. If it works successfully, we can

python manage.py shell
In [1]: from django.contrib.gis.geoip2 import GeoIP2                                     

In [2]: g = GeoIP2()                                                                     

In [3]: g.city("72.14.207.99")                                                           
Out[3]:                                                                                  
{'city': 'Mountain View',
 'continent_code': 'NA',
 'continent_name': 'North America',
 'country_code': 'US',
 'country_name': 'United States',
 'dma_code': None,
 'is_in_european_union': False,
 'latitude': 37.4225,
 'longitude': -122.085,
 'postal_code': None,
 'region': None,
 'time_zone': None}

In [4]: 

Great! Now that we know it works, let’s create a simple API where users send the IP and we answer with its location.

GeoIP API

To simplify this step, let’s skip using serializers and pass the IP as a GET parameter, in the views file

from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.exceptions import APIException
from common.utils import *

from django.contrib.gis.geoip2 import GeoIP2, GeoIP2Exception

class GeoIPView(APIView):
    permission_classes = ( )
    renderer_classes = [CustomRenderer, BrowsableAPIRenderer]

    def get(self, request, *args, **kwargs):
        ip_address = request.query_params.get('ip', None)
        if not ip_address:
            raise APIException("The 'ip' query parameter is required.")

        try:
            g = GeoIP2()
            # You can use .city(ip), .country(ip), etc. depending on the data you need
            geoip_data = g.city(ip_address)
            return Response(geoip_data)
        except GeoIP2Exception:
            # This exception is raised if the IP address is not in the database.
            raise APIException(f"Information for IP address '{ip_address}' not found.")

app_main/views.py

We are getting the IP from GET parameters, then passing it to GeoIP, and responding with its direct response.
Let’s assign a URL for the view

from django.urls import path, include
from .views import *

urlpatterns = [
    path('geoip/', GeoIPView.as_view()),
]

app_main/urls.py

from django.contrib import admin
from django.urls import path, include

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

saas/urls.py

Let’s try it out!

GeoIP api

great!
Let’s do something more entertaining, let’s edit the API to respond with the user’s IP location if the IP parameter is not provided!

from django.contrib.gis.geoip2 import GeoIP2, GeoIP2Exception

class GeoIPView(APIView):
    permission_classes = ( )
    renderer_classes = [CustomRenderer, BrowsableAPIRenderer]

    def _get_client_ip(self, request):
        """Helper method to get the client's real IP address from the request."""
        # Check for the X-Forwarded-For header, which is used by proxies
        x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
        if x_forwarded_for:
            # The header can contain a comma-separated list of IPs; the first is the client
            ip = x_forwarded_for.split(',')[0]
        else:
            # If not behind a proxy, use the standard REMOTE_ADDR
            ip = request.META.get('REMOTE_ADDR')
        return ip

    def get(self, request, *args, **kwargs):
        ip_address = request.query_params.get('ip', None)
        if not ip_address:
            ip_address = self._get_client_ip(request)


        try:
            g = GeoIP2()
            # You can use .city(ip), .country(ip), etc. depending on the data you need
            geoip_data = g.city(ip_address)
            return Response(geoip_data)
        except Exception as e:
            # This exception is raised if the IP address is not in the database.
            raise APIException(f"Information for IP address '{ip_address}' not found.")

app_main/views.py

We start by looking for ip parameter in the GET request; if it exists, we look for it; if not, we will try to get the request IP.
_get_client_ip will get the request IP, locally it will get the local IP, if online, it will get the user request IP.

Let’s try it online

GET /api/geoip/

HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "status": "success",
    "code": 200,
    "data": {
        "city": "Remscheid",
        "continent_code": "EU",
        "continent_name": "Europe",
        "country_code": "DE",
        "country_name": "Germany",
        "dma_code": null,
        "is_in_european_union": true,
        "latitude": 51.1798,
        "longitude": 7.1925,
        "postal_code": null,
        "region": null,
        "time_zone": null
    },
    "message": null
}

GET /api/geoip/

HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "status": "success",
    "code": 200,
    "data": {
        "city": "Nicosia",
        "continent_code": "EU",
        "continent_name": "Europe",
        "country_code": "CY",
        "country_name": "Cyprus",
        "dma_code": null,
        "is_in_european_union": true,
        "latitude": 35.1728,
        "longitude": 33.354,
        "postal_code": null,
        "region": null,
        "time_zone": null
    },
    "message": null
}

Bonus, User IP 🤓

We can add the user IP to the response by adding one line to the view as

...
        try:
            g = GeoIP2()
            # You can use .city(ip), .country(ip), etc. depending on the data you need
            geoip_data = g.city(ip_address)
            geoip_data['ip'] = ip_address
            return Response(geoip_data)
        except Exception as e:
            # This exception is raised if the IP address is not in the database.
            raise APIException(f"Information for IP address '{ip_address}' not found.")

app_main/views.py

the response

GET /api/geoip/

HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "status": "success",
    "code": 200,
    "data": {
        "city": "Beirut",
        "continent_code": "AS",
        "continent_name": "Asia",
        "country_code": "LB",
        "country_name": "Lebanon",
        "dma_code": null,
        "is_in_european_union": false,
        "latitude": 33.8938,
        "longitude": 35.5018,
        "postal_code": null,
        "region": null,
        "time_zone": null,
        "ip": "185.227.133.12"
    },
    "message": null
}

That is it for this tutorial. We still need to add the SaaS users/ tokens/ stats management, which we will discuss in another tutorial, so

Stay tuned 😎


This content originally appeared on DEV Community and was authored by Saad Alkentar