Backend API
This guide explains how to define API endpoints for use in both the Dashboard and on customer-facing sites.
Overview
Customer environments are located in /api-v3/src/customers and each contain the following layout:
customer-a/
├── apis/
│ ├── admin.py
│ └── public.py
├── helpers/
├── types/
├── app.py
├── schemas.py
This structure should be kept intact as much as possible and makes clear separation between the following concepts:
- APIs: where you place your dashboard-facing or customer-facing code.
- Helpers: helper classes to use in your code, keeping your endpoints short and on point.
- Types: types that help keeping your code robust and prevent invalid data from getting into the system
- App: a simple app definition, can usually be left as-is
- Schemas: schema definitions for Bobs
APIs
This is the most important place and typically where most of the work takes place. It contains endpoints for either the admin (i.e. dashboard) or public access from e.g. a customer's web site.
Creating an Endpoint
Creating an endpoint is as simple as creating an async function and decorating with the @api.route giving it
its unique URL and input parameters as read from the body of the incoming request.
# admin.py
@api.route("/test-endpoint")
async def test_endpoint():
return {"hello": "world"}
Input Arguments
All endpoints accept POST requests with a JSON body. Typically, the body is a simple JSON object:
{
"some": "string",
"a": 12,
"timestamp": "2020-01-01T21:12:00Z",
"tju_is_cool": true
}
To access these values, define function arguments matching the JSON keys:
async def test_endpoint(some, a, timestamp, tju_is_cool):
# your code here
Adding Type Hints
While Python remain a duck typed language, adding type hints to your code will significantly improve its robustness and the ability to catch errors early. For endpoints these hints are key and help validate the input before your function is called.
Therefore we should always strictly define the types we want to receive:
async def test_endpoint(some: str, a: int, timestamp: datetime, tju_is_cool: bool):
# your code here
This ensures that the API engine can validate the incoming request against your function signature
and appropriately return a 400 Bad Request in case types do not match.
It will include additional diagnostics telling you which parameters that are invalid or missing.
{
"code": "bad_request",
"invalid": ["a"],
"missing": ["timestamp", "tju_is_cool"]
}
Optional Arguments
To make arguments optional, simply add | None to the type hint:
async def test_endpoint(
some: str,
a: int,
timestamp: datetime | None,
tju_is_cool: bool | None
):
# your code here
This means that a and some are required while timestamp and tju_is_cool can be left out of the
request body.
Complex Input: Nested Objects & Arrays
If you have an endpoint that needs to take on a lot of parameters or complex data structures, it might be easier to wrap
the entire body in custom type. To define one, simply create a class definition that inherits from
SnappyObject:
class TestItemType(SnappyObject):
name: str
size: str
description: str | None = None
class TestType(SnappyObject):
title: str
items: list[TestItemType]
You can then use that type in your endpoint:
async def test_endpoint(body: TestType):
return { "number_of_items": len(body.items) }
And you can receive:
{
"title": "Hello",
"items": [
{ "name": "World", "size": "small" },
{ "name": "Galaxy", "size": "bigger" },
{ "name": "Universe", "size": "biggest", "description": "argh" }
]
}
Sharing Code Across Projects
Each customer’s endpoints are isolated, but shared logic should be reused. Use the SnappyUtil class for common functionality.
Add it as an argument to your endpoint:
async def test_endpoint(a_value: str, snpy: SnappyUtil):
# your code here
SnappyUtil provides:
| Pattern | Purpose |
|---|---|
snpy.bobs.* | Work with Bobs and customer data |
snpy.sms.* | Send SMS via Twilio |
snpy.emails.* | Send emails via SendGrid |
snpy.docs.* | Manage documents, AI embeddings, etc. |
snpy.ai.* | Perform AI-based tasks |
Example: Sending an SMS, logging it and returning the logged Bob
@api.route("/send-sms")
async def send_sms(number: str, msg: str, snpy: SnappyUtil):
snpy.sms.send(number, msg)
bob = snpy.bobs.merge({
"flavor": "sms_log",
"to": number,
"message": msg
})
return bob
Helpers
In this folder you can add helper code, which assists your endpoints with long processes, so that the actual endpoint code is immediately clear in what it does.
The simplest way to add a helper is to inherit the SnappyHelper class.
class MyHelper(SnappyHelper):
def do_something_helpful(self):
# code here
Using the helper simple requires adding it as an argument to your function.
async def help_me(question: str, helper: MyHelper):
helper.do_something_helpful(question)
return { "success": True }
Additionally you have direct access to SnappyUtil through the snpy member and
can use all of the predefined helper methods from there.
class MyHelper(SnappyHelper):
def do_something_helpful(self, question: str):
self.snpy.bobs.merge({
"flavor": "help_log",
"question": question
})