Deploy a Serverless Python JSON API to Zeit Now

Create a Serverless JSON API in Python and deploy it to Zeit Now, a platform for global serverless deployments

Deploy a Serverless Python JSON API to Zeit Now - Post illustration

This post explains how to set up a Serverless Python app serving as a starting point for building a REST API.

Project source code available at...
visini/barebone-serverless-api-zeit-now

What we’ll use:

  • Zeit Now, a serverless platform
  • Python 3 and PyCharm as an IDE
  • PostgreSQL and psycopg2, the most popular PostgreSQL adapter for Python

The end result is a fully working Serverless JSON API serving as a starting point for your project (for instance, hosting your Python microservices without having to provision any servers).

Preliminary Steps

Make sure that you have signed up for a Zeit account and have installed the command line interface, Now CLI. In order to do that, and automatically set up your local environment with the correct credentials to deploy to Zeit Now, you can download and install the macOS client, Zeit Desktop. You’ll have to sign in via the app from the menu bar. Now Desktop also installs Now CLI (the command line interface for Now). We’ll use the Now CLI to deploy our API to Zeit Now.

Project Setup

In this step we will install all requirements on our local machine. I’m using macOS 10.14, so the commands that follow might not correspond exactly to your environment. Run the following commands in Terminal:

Terminal
cd ~/PycharmProjects
mkdir barebone-serverless-api-zeit-now
cd barebone-serverless-api-zeit-now
touch now.json index.py debugserver.py
charm .

The first step is to configure the deployment settings for Zeit Now. Add the following configuration:

now.json
JSON
{
  "version": 2,
  "name": "barebone-serverless-api-zeit-now",
  "builds": [
    {
      "src": "*.py",
      "use": "@now/python",
      "config": { "maxLambdaSize": "10mb" }
    }
  ]
}

We’ll include an entry point to our API. For now, it will output a basic „Hello World“ example. Add the following code:

index.py
Python
from http.server import BaseHTTPRequestHandler
import debugserver
import json

class handler(BaseHTTPRequestHandler):

    def do_GET(self):

        self.send_response(200)
        self.send_header("Content-type", "application/json")
        self.end_headers()

        response = {"hello": "world"}
        self.wfile.write(json.dumps(response).encode("utf-8"))

        return

if __name__ == '__main__':
    debugserver.serve(handler)

In order to test our project locally before deploying to Zeit Now we have to create a custom server to expose our API locally. This enables us to test a specific route of our API via http://localhost:8888 to make sure everything is working as expected before deployment. Add the following in debugserver.py:

debugserver.py
Python
from http.server import HTTPServer
def serve(handler):
    try:
        port = 8888
        debugserver = HTTPServer(('', port), handler)
        print('Started httpserver on port', port)
        print('http://localhost:' + str(port))
        debugserver.serve_forever()
    except KeyboardInterrupt:
        print('^C received, shutting down the web server')
        debugserver.socket.close()

Now, we can test if everything is working properly by executing the following command:

Terminal
python3 index.py

The above command will run the server locally (via debugserver.serve()). You’ll be able to check if everything is working by navigating to http://localhost:8888 and see the response {"hello": "world"}:

Hello World
Hello World

You can quit the server with CTRL + C. Deploying the app to Zeit Now takes mere seconds. Just run the following command in Terminal in your project folder.

Terminal
now

That’s it! As you can see, the deployment only took 16 seconds:

Deployment
Deployment

You can check if everything was correctly deployed to Zeit Now by navigating to the URL you can retrieve from Terminal (it is automatically copied to your clipboard). It should give the identical output {"hello": "world"}, as already seen in the previous step.

In the next steps, we will connect our API with a PostgreSQL database, set up an additional endpoint to retrieve data and finish our boilerplate.

Database Setup

Normally, an API needs to perform operations on a database (i.e. create, read, update and delete, see CRUD). That’s why we will connect our API to a database, in our case a remote PostgreSQL database hosted on AWS RDS.

In our PostgreSQL database, we’ll create a simple table called blog_posts for our purposes by running the following SQL code:

blog_posts.sql
SQL
CREATE TABLE blog_posts (
   id serial primary key,
   title text,
   post text
);
INSERT INTO blog_posts (title, post) VALUES ('A title', 'Lorem ipsum dolor');
INSERT INTO blog_posts (title, post) VALUES ('Another title', 'Ipsum dolor sit amet');

The next step is to download a custom compiled psycopg2 C-library for Python. Using psycopg2 via requirements.txt (conventional approach for installing modules) will not work. We will need to use the library from this GitHub repository. Be sure to download the repository to your machine, and (as we use Python 3) place the folder psycopg2-3.6 at the same level of your now.json file (root project folder). Then, rename the folder so it’s called psycopg2. As Zeit Now uses AWS Lambda to deploy your project, we need to use this custom pre-compiled library. Many thanks to Jeff Kehler.

The next step involves creating a database connector file, and setting up a separate route. Execute the following commands:

Terminal
touch database.py blog_posts.py

When testing our project locally, we need to use the regular psycopg2 module, but when deployed, we need to add instructions to load the custom pre-compiled module installed above (note the first try/except clause). Add the following code (be sure to modify your database credentials accordingly):

database.py
Python
import importlib.machinery
from distutils.sysconfig import get_python_lib

try:
    psycopg2 = importlib.machinery.SourceFileLoader('psycopg2', get_python_lib()+'/psycopg2/__init__.py').load_module()
except:
    import psycopg2

# connect to database
def connect():
    dbname = "xxx"
    user = "xxx"
    host = "xxx.eu-west-1.rds.amazonaws.com"
    password = "xxx"
    try:
        return psycopg2.connect("dbname='{}' user='{}' host='{}' password='{}'".format(dbname, user, host, password))
    except:
        print("I am unable to connect to the database")

Now we will create an endpoint which will retrieve all blogposts from the database. In blog_posts.py, add the following code:

blog_posts.py
Python
from http.server import BaseHTTPRequestHandler
import json
import database
import debugserver

class handler(BaseHTTPRequestHandler):

    def do_GET(self):

        conn = database.connect()

        self.send_response(200)
        self.send_header("Content-type", "application/json")
        self.end_headers()

        cur = conn.cursor()
        cur.execute("""SELECT * from blog_posts""")
        columns = ('id', 'title', 'post')
        results = []
        for row in cur.fetchall():
            results.append(dict(zip(columns, row)))

        self.wfile.write(json.dumps(results).encode("utf-8"))

        return

if __name__ == '__main__':
    debugserver.serve(handler)

You can test if everything is working as expected by running the following command (this will launch the debug server):

Terminal
python3 blog_posts.py

By navigating to http://localhost:8888, you can verify that the entries stored in the respective table are retrieved:

Blog Posts
Blog Posts

The only step remaining is to deploy the API to Zeit Now and expose it to the public so you can connect it to your frontend.

Deploy the API to Zeit Now

Because we have added a new route (as defined in the file blog_posts.py), we need to reflect this change in our now.json file, mapping the route endpoint to the python file. Edit the file so it contains the following code:

now.json
JSON
{
  "version": 2,
  "name": "barebone-serverless-api-zeit-now",
  "builds": [
    {
      "src": "*.py",
      "use": "@now/python",
      "config": { "maxLambdaSize": "10mb" }
    }
  ],
  "routes": [{ "src": "/blog_posts", "dest": "/blog_posts.py" }]
}

All that remains is to deploy our code to Zeit Now. Again, this is extremely convenient and can be done by running the following command again:

Terminal
now

Now, accessing the URL returned by the Now CLI will return the „Hello World“ example (as defined in index.py), navigating to /blog_posts will return the database entries in JSON format:

Blog Posts Zeit
Blog Posts Zeit

You can check logs by accessing /_logs of your deployment URL. This will redirect to the logs accessible on your Zeit account (logs are not public and can only be accessed by you when being logged in to your account).

You’ve successfully set up a serverless JSON API serving as a starting point for your project. You can add more endpoints / routes according to the specifications of your projects. P.S.: You can also clone the GitHub repository to get a head start for your next project, instead of following the steps above – it contains all the files from this tutorial, including the pre-compiled psycopg2 library.

Zeit Now is free up to a certain point – check out their pricing on how it scales as you grow.

Project source code available at...
visini/barebone-serverless-api-zeit-now

I hope you enjoyed this article  -  please let me know if you have any questions or if you run into any issue. Thanks for reading!

© 2024 Camillo Visini
Imprint