August 7: Facade Layer Design
Last updated: Oct 9, 2024
Contents
Hello everyone, welcome back! Today was a productive day for me as I got to work with pure SQL and fully implemented metering logic within a MySQL Stored Procedure. More about that coming soon!
For now, let’s talk about the facade design pattern. In my workplace, we have a facade layer implementation, which serves as a central point of communication for the frontend layer (and some backend APIs). The job of this layer is to provide endpoints of internal APIs to the public through the facade, hence the name. This is implemented in FastAPI.
Currently, in the facade layer, when someone needs to add a new layer, they create a FastAPI endpoint, add some function to perform pre-processing with data if necessary, call another service(s), then post-process the data if required, and serve it. Simple enough, right?
This got me thinking: this could be done using a JSON config where you just define the endpoints you want to expose, the backend endpoint that the request will be routed through, and run pre/post-processing functions if required. I have created a POC of this design in FastAPI, so let’s talk about it.
File Structure
├── helpers
├── main.py
├── Readme.md
├── routes.json
├── services
│ ├── function_registry.py
│ ├── __init__.py
│ ├── postprocess
│ │ ├── products.py
│ ├── preprocess
The above is the directory structure of the codebase.
Components
routes.json
This is the JSON file that contains the route information.
{
"/api/products": {
"method": "GET",
"backend_url": "https://fakestoreapi.com/products/",
"pre_process": "preprocess_products",
"post_process": "postprocess_products"
},
"/api/products/{id}": {
"method": "GET",
"backend_url": "https://fakestoreapi.com/products/{id}"
}
}
In the JSON structure above, each key represents a route we want to expose through the facade. The backend_url
field specifies where the request will be redirected. The optional fields pre_process
and post_process
define functions that will be invoked to pre-process the request and post-process the data, respectively. Though the config can handle path parameters, this feature is not yet implemented in the POC.
function_registry.py
function_registry = {}
def register_function(func):
function_registry[func.__name__] = func
return func
This script includes two components: a global function store called function_registry
and a register_function
decorator. The decorator adds a function’s name to the function registry, enabling us to specify which functions to call in the routes.json file.
postprocess/products.py
from services.function_registry import register_function
@register_function
async def postprocess_products(data: dict):
print("Post-processing procucts response")
# Implement post-processing logic for users
new_data = []
for d in data:
new_data.append({
k:v for k, v in d.items() if k in ("id", "title", "price")
})
return n
We implement the decorator on a postprocessor to register it in function_registory
.
main.py
app = FastAPI()
# Load route configurations from a JSON file
with open("routes.json") as f:
route_configs = json.load(f)
async def facade_handler(
request: Request,
config: Dict[str, Any],
pre_process: Callable = None,
post_process: Callable = None
):
# Pre-processing
if pre_process:
await pre_process(request)
# Make request to backend API
async with httpx.AsyncClient() as client:
response = await client.request(
method=request.method,
url=config["backend_url"],
# headers=request.headers,
params=request.query_params,
)
response_data = response.json()
# Post-processing
if post_process:
response_data = await post_process(response_data)
return JSONResponse(content=response_data, status_code=response.status_code)
def create_route_handler(config, pre_process, post_process):
async def route_handler(request: Request):
return await facade_handler(request, config, pre_process, post_process)
return route_handler
# Dynamic route generation based on config
for route, config in route_configs.items():
method = config.get("method", "GET")
pre_process = function_registry.get(config.get("pre_process"))
post_process = function_registry.get(config.get("post_process"))
app.add_api_route(
route,
create_route_handler(config, pre_process, post_process),
methods=[method]
)
The above piece of code connects all the dots. It has three parts:
- Loading Route Configs: First, we load the route configs from the
routes.json
file. - Facade Handler: We have a function, appropriately named
facade_handler
, which handles the incoming request as required. - Dynamic Route Generation: Here, we get the necessary configs and use FastAPI’s
add_api_route
method to dynamically add the route.
This structure allows us to seamlessly integrate the facade design pattern with FastAPI by defining the routes and processing logic in a JSON configuration file. You can go to the /docs endpoint to see the generated endpoints and test them out.
I hope this was informative to you as it was to me. See you soon, peace out!