PolarSPARC |
Quick Primer on Starlette
Bhaskar S | 04/12/2025 |
Overview
Regular web servers have no way of executing any Python code. This is where the Asynchronous Server Gateway Interface (or ASGI) comes into play. It defines a well-defined standard interface for executing asynchronous Python code via the web server. The well defined ASGI interface could be implemented by any web server provider as a pluggable module for intercepting and routing the web requests to an ASGI server that can invoke any Python code in a standard way.
Starlette is a lightweight ASGI framework that can be plugged into any web server for executing asynchronous Python web services in a standard way !!!
In this primer, we will demonstrate how one can effectively setup and use the Starlette framework.
Installation and Setup
The installation and setup will be on a Ubuntu 24.04 LTS based Linux desktop. Ensure that Python 3.x programming language is installed and setup on the desktop.
In addition, ensure that the Linux command-line utility curl is also installed and setup on the desktop.
To install the necessary Python packages, execute the following command in a terminal window:
$ pip install starlette sse-starlette uvicorn
For our demonstration, we will create a directory called Starlette under the users home directory by executing the following command in a terminal window:
$ mkdir -p $HOME/Starlette/html
This completes all the necessary installation and setup for the Starlette hands-on demonstration.
Hands-on with Starlette
In the following sections, we will get our hands dirty with the Starlette framework. So, without further ado, let us get started !!!
The following is our first Starlette Python code:
# # @Author: Bhaskar S # @Blog: https://www.polarsparc.com # @Date: 06 April 2025 # import logging import uvicorn from starlette.applications import Starlette from starlette.responses import PlainTextResponse from starlette.routing import Route logging.basicConfig(format='%(levelname)s %(asctime)s - %(message)s', level=logging.INFO) logger = logging.getLogger('hello') async def hello(request): logger.info(f'Received request: {request.method}') return PlainTextResponse('Hello Starlette !!!') if __name__ == '__main__': logger.info('Starting server...') app = Starlette(routes=[ Route('/', hello), ]) uvicorn.run(app, host='127.0.0.1', port=8000)
To execute the above Python code, execute the following command in a terminal window:
$ python sample-1.py
The following would be the typical output:
INFO 2025-04-12 08:55:52,963 - Starting server... INFO: Started server process [23644] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Open a terminal window and execute the following command:
$ curl -v http://127.0.0.1:8000/
The following would be the typical output:
* Trying 127.0.0.1:8000... * Connected to 127.0.0.1 (127.0.0.1) port 8000 > GET / HTTP/1.1 > Host: 127.0.0.1:8000 > User-Agent: curl/8.5.0 > Accept: */* > < HTTP/1.1 200 OK < date: Sat, 12 Apr 2025 14:14:00 GMT < server: uvicorn < content-length: 19 < content-type: text/plain; charset=utf-8 < * Connection #0 to host 127.0.0.1 left intact Hello Starlette !!!
The code from sample-1.py above needs some explanation:
The application class Starlette implements the well defined ASGI interface, which ties all the other core functionality together so that it can be integrated with the web server.
The Starlette framework has a simple but very capable request routing system. A routing table is defined as a list of routes, and passed when instantiating the Starlette application.
The parameter routes is the list of routes, where each route is a HTTP endpoint to a Python function mapping for handling the incoming HTTP requests.
The class PlainTextResponse takes in some string text or bytes and returns a plain text HTTP response to the calling client.
The function call ubvicorn.run() takes as input an instance of ASGI (an instance of class Starlette in this case) and launches a web server on the local host (127.0.0.1) on the port 8000.
Next, the following is our second Starlette Python code that demonstrates multiple HTTP endpoints with different HTTP response types:
# # @Author: Bhaskar S # @Blog: https://www.polarsparc.com # @Date: 06 April 2025 # import logging import uvicorn from starlette.applications import Starlette from starlette.middleware.cors import CORSMiddleware from starlette.responses import JSONResponse, PlainTextResponse from starlette.routing import Route logging.basicConfig(format='%(levelname)s %(asctime)s - %(message)s', level=logging.INFO) logger = logging.getLogger('hello') async def hello(request): logger.info(f'Received request: {request.method} on {request.url.path}') return PlainTextResponse('Hello Starlette !!!') # Uses path param /user/{name} async def hello_name(request): logger.info(f'Received request: {request.method} on {request.url.path}') name = request.path_params.get('name') return JSONResponse({'response': f'Hello {name} !!!'}) if __name__ == '__main__': logger.info('Starting server...') routes = [ Route('/', hello), Route('/user/{name}', hello_name) ] app = Starlette(routes=routes) app.add_middleware(CORSMiddleware, allow_origins=['*']) uvicorn.run(app, host='127.0.0.1', port=8000)
To execute the above Python code, execute the following command in a terminal window:
$ python sample-2.py
The output would be similar to that of Output.1 from above.
Open a terminal window and execute the following command:
$ curl -v http://127.0.0.1:8000/user/Vader
The following would be the typical output:
* Trying 127.0.0.1:8000... * Connected to 127.0.0.1 (127.0.0.1) port 8000 > GET /user/Vader HTTP/1.1 > Host: 127.0.0.1:8000 > User-Agent: curl/8.5.0 > Accept: */* > < HTTP/1.1 200 OK < date: Sat, 12 Apr 2025 15:19:17 GMT < server: uvicorn < content-length: 30 < content-type: application/json < * Connection #0 to host 127.0.0.1 left intact {"response":"Hello Vader !!!"}
The code from sample-2.py above needs some explanation:
The class Route defines a route in the Starlette application and is essentially a mapping of a HTTP endpoint to a HTTP request handler Python function. It uses the following parameters:
The class JSONResponse takes in JSON string text and returns an "application/json" encoded HTTP response to the calling client.
An instance of class CORSMiddleware adds support for handling Cross-Origin Resource Sharing (CORS), a mechanism that enables web apps running from one domain to access resources on a different domain.
Moving along, the following is our third Starlette Python code that enables one to serve static HTML file(s) from a directory:
# # @Author: Bhaskar S # @Blog: https://www.polarsparc.com # @Date: 06 April 2025 # import logging import uvicorn from starlette.applications import Starlette from starlette.routing import Mount from starlette.staticfiles import StaticFiles logging.basicConfig(format='%(levelname)s %(asctime)s - %(message)s', level=logging.INFO) logger = logging.getLogger('hello') if __name__ == '__main__': logger.info('Starting server...') routes = [ Mount('/', app=StaticFiles(directory='html', html=True), name='html'), ] app = Starlette(routes=routes) uvicorn.run(app, host='127.0.0.1', port=8000)
To execute the above Python code, execute the following command in a terminal window:
$ python sample-3.py
The output would be similar to that of Output.1 from above.
Open a terminal window and execute the following command:
$ curl -v http://127.0.0.1:8000/
The following would be the typical output:
* Trying 127.0.0.1:8000... * Connected to 127.0.0.1 (127.0.0.1) port 8000 > GET / HTTP/1.1 > Host: 127.0.0.1:8000 > User-Agent: curl/8.5.0 > Accept: */* > < HTTP/1.1 200 OK < date: Sat, 12 Apr 2025 17:03:43 GMT < server: uvicorn < content-type: text/html; charset=utf-8 < accept-ranges: bytes < content-length: 433 < last-modified: Sat, 12 Apr 2025 01:03:00 GMT < etag: "30a61f18583f5047c27d53c3ce200504" < <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Hello Starlette</title> <style> body { font-family: Arial, sans-serif; margin: 25px; } h3 { font-size: 25px; font-style: italic; } </style> </head> <body> <h3>Hello Starlette</h3> </body> </html> * Connection #0 to host 127.0.0.1 left intact
The code from sample-3.py above needs some explanation:
An instance of the class StaticFiles enables onefor serving files in a given directory and uses the following parameters:
An instance of the class Mount enables one to map a HTTP endpoint prefix to either a list of Route objects or an instance of StaticFiles.
Moving along, the following is our fourth Starlette Python code demonstrates how one can implement basic authentication to protect HTTP endpoint(s):
# # @Author: Bhaskar S # @Blog: https://www.polarsparc.com # @Date: 06 April 2025 # import base64 import binascii import logging import uvicorn from starlette.applications import Starlette from starlette.authentication import AuthenticationBackend, AuthenticationError, AuthCredentials, SimpleUser from starlette.middleware import Middleware from starlette.middleware.authentication import AuthenticationMiddleware from starlette.responses import JSONResponse, Response from starlette.routing import Route logging.basicConfig(format='%(levelname)s %(asctime)s - %(message)s', level=logging.INFO) logger = logging.getLogger('hello') # Test credential: admin:s3cr3t! => YWRtaW46czNjcjN0IQ== # Request header: Authorization: Basic YWRtaW46czNjcjN0IQ== class BasicAuthBackend(AuthenticationBackend): async def authenticate(self, request): if 'Authorization' not in request.headers: return None auth = request.headers['Authorization'] try: scheme, credentials = auth.split() if scheme.lower() != 'basic': return None decoded = base64.b64decode(credentials).decode('ascii') except (ValueError, UnicodeDecodeError, binascii.Error) as ex: raise AuthenticationError('Invalid Basic Auth') username, _, password = decoded.partition(':') if not username or not password: raise AuthenticationError('Invalid Basic Auth Credentials') if password != 's3cr3t!': raise AuthenticationError('Invalid Basic Auth Password') return AuthCredentials(['authenticated']), SimpleUser(username) async def protected_hello(request): logger.info(f'Received request: {request.method}') if request.user.is_authenticated: return JSONResponse({'message': 'Hello Protected Starlette !!!'}) return Response(headers={'WWW-Authenticate': 'Basic'}, status_code=401) if __name__ == '__main__': logger.info('Starting server...') routes = [ Route('/', protected_hello), ] middleware = [ Middleware(AuthenticationMiddleware, backend=BasicAuthBackend()) ] app = Starlette(routes=routes, middleware=middleware) uvicorn.run(app, host='127.0.0.1', port=8000)
To execute the above Python code, execute the following command in a terminal window:
$ python sample-4.py
The output would be similar to that of Output.1 from above.
Open a terminal window and execute the following command:
$ curl -v http://127.0.0.1:8000/
The following would be the typical output:
* Trying 127.0.0.1:8000... * Connected to 127.0.0.1 (127.0.0.1) port 8000 > GET / HTTP/1.1 > Host: 127.0.0.1:8000 > User-Agent: curl/8.5.0 > Accept: */* > < HTTP/1.1 401 Unauthorized < date: Sat, 12 Apr 2025 17:32:55 GMT < server: uvicorn < www-authenticate: Basic < content-length: 0 < * Connection #0 to host 127.0.0.1 left intact
Notice the HTTP status code of 401 in the response above !!!
Once again, execute the following command, this time providing the correct credentials:
$ curl -v http://127.0.0.1:8000/ -u admin:s3cr3t!
The following would be the typical output:
* Trying 127.0.0.1:8000... * Connected to 127.0.0.1 (127.0.0.1) port 8000 * Server auth using Basic with user 'admin' > GET / HTTP/1.1 > Host: 127.0.0.1:8000 > Authorization: Basic YWRtaW46czNjcjN0IQ== > User-Agent: curl/8.5.0 > Accept: */* > < HTTP/1.1 200 OK < date: Sat, 12 Apr 2025 17:36:43 GMT < server: uvicorn < content-length: 43 < content-type: application/json < * Connection #0 to host 127.0.0.1 left intact {"message":"Hello Protected Starlette !!!"}
The code from sample-4.py above needs some explanation:
The class AuthenticationBackend is the base class for all authentication methods. One can create a custom authentication method by subclassing from this class and overriding the class method authenticate which takes in a request parameter.
The class AuthenticationMiddleware offers a powerful interface for handling authentication and permissions. To enforce authentication, one one must install the AuthenticationMiddleware with an appropriate authentication backend.
Shifting gears, the following is our fifth Starlette Python code that demonstrates how one can implement the Server Sent Events (or SSE) functionality:
# # @Author: Bhaskar S # @Blog: https://www.polarsparc.com # @Date: 06 April 2025 # import asyncio import logging import uvicorn from starlette.applications import Starlette from starlette.middleware.cors import CORSMiddleware from starlette.responses import HTMLResponse from starlette.requests import Request from starlette.routing import Route from sse_starlette.sse import EventSourceResponse logging.basicConfig(format='%(levelname)s %(asctime)s - %(message)s', level=logging.INFO) logger = logging.getLogger('sse') html_sse = """ <html> <body> <script> var evtSource = new EventSource("/stream"); evtSource.onmessage = function(evt) { document.getElementById('data').innerText += evt.data + " "; if (evt.data == 5) { evtSource.close(); } } </script> <h3>Response from server:</h3> <div id="data"></div> </body> </html> """ async def event_stream(request: Request): seq_no = 1 while True: logger.info('Check if client disconnected ...') if await request.is_disconnected(): logger.info('Client disconnected !!!') break await asyncio.sleep(1.5) data = dict(data=seq_no) logger.info(f'Event: {data}') yield data seq_no += 1 async def sse(request): logger.info(f'Received request: {request.method} on {request.url.path}') generator = event_stream(request) return EventSourceResponse(generator) async def home(request: Request): logger.info(f'Received request: {request.method} on {request.url.path}') return HTMLResponse(html_sse) if __name__ == '__main__': logger.info('Starting server...') routes = [ Route('/', home), Route('/stream', sse) ] app = Starlette(routes=routes) app.add_middleware(CORSMiddleware, allow_origins=['*']) uvicorn.run(app, host='127.0.0.1', port=8000)
To execute the above Python code, execute the following command in a terminal window:
$ python sample-5.py
The output would be similar to that of Output.1 from above.
Open a terminal window and execute the following command:
$ curl -v -N http://127.0.0.1:8000/stream
The following would be the typical output:
* Trying 127.0.0.1:8000... * Connected to 127.0.0.1 (127.0.0.1) port 8000 > GET /stream HTTP/1.1 > Host: 127.0.0.1:8000 > User-Agent: curl/8.5.0 > Accept: */* > < HTTP/1.1 200 OK < date: Sat, 12 Apr 2025 18:08:15 GMT < server: uvicorn < cache-control: no-store < connection: keep-alive < x-accel-buffering: no < content-type: text/event-stream; charset=utf-8 < transfer-encoding: chunked < data: 1 data: 2 data: 3 data: 4 data: 5 <CTRL-C>
Note the command line option -N is VERY important for this command to receive the stream of events from the web server !!!
Server Sent Events is a capability that is built into the HTML5 specification that allows the web server to push data events to a client over a single, long lived HTTP connection. Once a client initiates and is connected to the web server, it is a one-way communication from the server to the client.
With this, we conclude the various demonstrations on using the Starlette framework for building and deploying asynchronous web applications !!!
References