Python with Starlette
Cover Page
Back-end Page
Add dependencies
Change to your chatterd directory and add two packages including the PostgreSQL adaptor:
server$ cd ~/reactive/chatterd
server$ uv add pydantic-core 'psycopg[binary,pool]'
chatterd app
Edit your main.py file:
server$ vi main.py
Add the following import lines:
from contextlib import asynccontextmanager
from psycopg_pool import AsyncConnectionPool
Add the following lifespan() function to set up a pool of open connections to our PostgreSQL chatterdb
database. Maintaining a pool of open connections avoids the cost of opening and closing a connection on every
database operation. The password used in creating AsyncConnectionPool must match the one you used when
setting up Postgres earlier.
@asynccontextmanager
async def lifespan(server):
server.pool = AsyncConnectionPool("dbname=chatterdb user=chatter password=chattchatt host=localhost", open=False)
await server.pool.open()
yield
await server.pool.close()
Continuing in main.py, find the routing table routes and add two routes right after the routes for
/llmprompt. These serve Chatter’s two APIs: HTTP GET request, with URL endpoint getchatts, and HTTP
POST request, with endpoint postchatt. With each endpoint, we also specify which HTTP method is allowed
for that endpoint and the handler function assigned to it:
Route('/getchatts', handlers.getchatts, methods=['GET']),
Route('/postchatt', handlers.postchatt, methods=['POST']),
The functions getchatts() and postchatt() will be implemented in handlers.py.
Find the construction of the web server and provide it with thelifespan function defined above,
in addition to the routes routing table:
# must come after routes and lifespan definitions
server = Starlette(routes=routes, lifespan=lifespan)
We’re done with main.py. Save and exit the file.
handlers.py
Edit the file handlers.py:
server$ vi handlers.py
First add the following imports at the top of the file:
from dataclasses import dataclass
from datetime import datetime
from psycopg.errors import StringDataRightTruncation
from pydantic_core import to_jsonable_python
from typing import Optional
from uuid import UUID
import main
We define a Chatt class to help postchatt() deserialize JSON
received from clients. Add these linese right below the above:
@dataclass
class Chatt:
name: str
message: str
The @dataclass annotations, among other things, automatically provides the
class with a replace() constructor, allowing us to deserialize incoming
JSON object into the Chatt dataclass directly.
The handler getchatts() uses an open connection from the connection pool to query the database for
stored chatts. We obtain and use the connection with its context manager that automatically commits
all transactions if no exception has been raised. In the case of the cursor, its context manager
releases any resources used. Once all the rows from the database are retrieved, we insert the
resulting array of chatts into the response JSON object. We use ` to_jsonable_python() from the
pydantic-core package to convert UUIDs present in the chatts into strings for JSONResponse().
Python's built-in json.dumps() method cannot serialize UUIDs. Add getchatts() to your
handlers.py after the definition of the llmprompt()` function.
async def getchatts(request):
try:
async with main.server.pool.connection() as connection:
async with connection.cursor() as cursor:
await cursor.execute('SELECT name, message, id, time FROM chatts ORDER BY time ASC;')
return JSONResponse(to_jsonable_python(await cursor.fetchall()))
except Exception as err:
print(f'{err=}')
return JSONResponse(f'{type(err).__name__}: {str(err)}', status_code = 500)
Similarly, postchatt() receives a posted chatt in the expected JSON format, deserializes it into the
Chatt class, and inserts it into the database, using a connection from the pool. The UUID and time stamp
of each chatt are generated at insertion time. Add postchatt() to the end of your handlers.py:
async def postchatt(request):
try:
chatt = Chatt(**(await request.json()))
except Exception as err:
print(f'{err=}')
return JSONResponse(f'Unprocessable entity: {str(err)}', status_code=422)
try:
async with main.server.pool.connection() as connection:
async with connection.cursor() as cursor:
await cursor.execute('INSERT INTO chatts (name, message, id) VALUES '
'(%s, %s, gen_random_uuid());', (chatt.name, chatt.message))
return JSONResponse({})
except StringDataRightTruncation as err:
print(f'Message too long: {str(err)}')
return JSONResponse(f'Message too long: {str(err)}', status_code = 400)
except Exception as err:
print(f'{err=}')
return JSONResponse(f'{type(err).__name__}: {str(err)}', status_code = 500)
For more Python-PostgreSQL interaction, see Passing parameters to SQL queries.
We’re done with handlers.py. Save and exit the file.
Test run
To test run your server, launch it from the command line:
server$ sudo su
# You are now root, note the command-line prompt changed from '$' or '%' to '#'.
# You can do a lot of harm with all of root's privileges, so be very careful what you do.
server# source .venv/bin/activate
(chattterd) ubuntu@server:/home/ubuntu/reactive/chatterd# granian --host 0.0.0.0 --port 443 --interface asgi --ssl-certificate /home/ubuntu/reactive/chatterd.crt --ssl-keyfile /home/ubuntu/reactive/chatterd.key --access-log --workers-kill-timeout 1 main:server
# Hit ^C to end the test
(chattterd) ubuntu@server:/home/ubuntu/reactive/chatterd# exit
# So that you're no longer root.
server$
You can test your implementation following the instructions in the Testing Chatter APIs section.
References
- Connection pools
- Handling UUID Serialization Error In Python 3
- PEP 557 – Data Classes
- Asynchronous Postgres with Python, FastAPI, and Psycopg 3
- SQL
- FastAPI
| Prepared by Tiberiu Vilcu, Wendan Jiang, Alexander Wu, Benjamin Brengman, Ollie Elmgren, Luke Wassink, Mark Wassink, Nowrin Mohamed, Chenglin Li, Xin Jie ‘Joyce’ Liu, Yibo Pi, and Sugih Jamin | Last updated January 23rd, 2026 |