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:
Provider | 1 mil/month cost |
---|---|
https://www.ip2location.com | $27980 |
https://www.maxmind.com | $4664 |
https://ipinfo.io | $9324 |
https://ipgeolocation.io | $7488 |
https://db-ip.com | $2400 |
https://ipregistry.co | $1200 |
https://apiip.net | $312 |
https://ipdata.co | $1152 |
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
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!
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