Endpoint Groups
An endpoint group brings endpoints together: it basically handles routing.
- Overview
- endpoints
- url
- response_headers
- security_headers
- internal_casing
- external_casing
- authentication
- authorization
Overview
The endpoint group accepts a list of endpoints/endpoint groups and routes requests to them. You can set a URL for the endpoint group, and this becomes a URL prefix for all of the endpoints under it. Note that all routing is greedy, which means you want to put endpoints with more specific URLs first. Here’s an example of how you can use them to build a fully functional API that manages both users and companies. Each individual endpoint is defined for the purpose of the example, but note that in practice you could accomplish this same thing with much less code by using the RestfulApi endpoint:
import clearskies
from clearskies.validators import Required, Unique
from clearskies import columns
class Company(clearskies.Model):
id_column_name = "id"
backend = clearskies.backends.MemoryBackend()
id = columns.Uuid()
name = columns.String(
validators=[
Required(),
Unique(),
]
)
class User(clearskies.Model):
id_column_name = "id"
backend = clearskies.backends.MemoryBackend()
id = columns.Uuid()
name = columns.String(validators=[Required()])
username = columns.String(
validators=[
Required(),
Unique(),
]
)
age = columns.Integer(validators=[Required()])
created_at = columns.Created()
updated_at = columns.Updated()
company_id = columns.BelongsToId(
Company,
readable_parent_columns=["id", "name"],
validators=[Required()],
)
company = columns.BelongsToModel("company_id")
readable_user_column_names = [
"id",
"name",
"username",
"age",
"created_at",
"updated_at",
"company",
]
writeable_user_column_names = ["name", "username", "age", "company_id"]
users_api = clearskies.EndpointGroup(
[
clearskies.endpoints.Update(
model_class=User,
url="/:id",
readable_column_names=readable_user_column_names,
writeable_column_names=writeable_user_column_names,
),
clearskies.endpoints.Delete(
model_class=User,
url="/:id",
),
clearskies.endpoints.Get(
model_class=User,
url="/:id",
readable_column_names=readable_user_column_names,
),
clearskies.endpoints.Create(
model_class=User,
readable_column_names=readable_user_column_names,
writeable_column_names=writeable_user_column_names,
),
clearskies.endpoints.SimpleSearch(
model_class=User,
readable_column_names=readable_user_column_names,
sortable_column_names=readable_user_column_names,
searchable_column_names=readable_user_column_names,
default_sort_column_name="name",
),
],
url="users",
)
readable_company_column_names = ["id", "name"]
writeable_company_column_names = ["name"]
companies_api = clearskies.EndpointGroup(
[
clearskies.endpoints.Update(
model_class=Company,
url="/:id",
readable_column_names=readable_company_column_names,
writeable_column_names=writeable_company_column_names,
),
clearskies.endpoints.Delete(
model_class=Company,
url="/:id",
),
clearskies.endpoints.Get(
model_class=Company,
url="/:id",
readable_column_names=readable_company_column_names,
),
clearskies.endpoints.Create(
model_class=Company,
readable_column_names=readable_company_column_names,
writeable_column_names=writeable_company_column_names,
),
clearskies.endpoints.SimpleSearch(
model_class=Company,
readable_column_names=readable_company_column_names,
sortable_column_names=readable_company_column_names,
searchable_column_names=readable_company_column_names,
default_sort_column_name="name",
),
],
url="companies",
)
wsgi = clearskies.contexts.WsgiRef(clearskies.EndpointGroup([users_api, companies_api]))
wsgi()
Usage then works exactly as expected:
$ curl 'http://localhost:8080/companies' -d '{"name": "Box Store"}' | jq
{
"status": "success",
"error": "",
"data": {
"id": "f073ee4d-318d-4e0b-a796-f450c40aa771",
"name": "Box Store"
},
"pagination": {},
"input_errors": {}
}
curl 'http://localhost:8080/users' -d '{"name": "Bob Brown", "username": "bobbrown", "age": 25, "company_id": "f073ee4d-318d-4e0b-a796-f450c40aa771"}'
curl 'http://localhost:8080/users' -d '{"name": "Jane Doe", "username": "janedoe", "age": 32, "company_id": "f073ee4d-318d-4e0b-a796-f450c40aa771"}'
$ curl 'http://localhost:8080/users' | jq
{
"status": "success",
"error": "",
"data": [
{
"id": "68cbb9e9-689a-4ae0-af77-d60e4cb344f1",
"name": "Bob Brown",
"username": "bobbrown",
"age": 25,
"created_at": "2025-06-08T10:40:37+00:00",
"updated_at": "2025-06-08T10:40:37+00:00",
"company": {
"id": "f073ee4d-318d-4e0b-a796-f450c40aa771",
"name": "Box Store"
}
},
{
"id": "e69c4ebf-38b1-40d2-b523-5d58f5befc7b",
"name": "Jane Doe",
"username": "janedoe",
"age": 32,
"created_at": "2025-06-08T10:41:04+00:00",
"updated_at": "2025-06-08T10:41:04+00:00",
"company": {
"id": "f073ee4d-318d-4e0b-a796-f450c40aa771",
"name": "Box Store"
}
}
],
"pagination": {
"number_results": 2,
"limit": 50,
"next_page": {}
},
"input_errors": {}
}
endpoints
Required
The list of endpoints connected to this endpoint group
url
Optional
The base URL for the endpoint group.
This URL is added as a prefix to all endpoints attached to the group. This includes any named URL parameters:
response_headers
Optional
Set some response headers that should be returned for this endpoint.
Provide a list of response headers to return to the caller when this endpoint is executed. This should be given a list containing a combination of strings or callables that return a list of strings. The strings in question should be headers formatted as “key: value”. If you attach a callable, it can accept any of the standard dependencies or context-specific values like any other callable in a clearskies application:
def custom_headers(query_parameters):
some_value = "yes" if query_parameters.get("stuff") else "no"
return [f"x-custom: {some_value}", "content-type: application/custom"]
endpoint = clearskies.endpoints.Callable(
lambda: {"hello": "world"},
response_headers=custom_headers,
)
wsgi = clearskies.contexts.WsgiRef(endpoint)
wsgi()
security_headers
Optional
Configure standard security headers to be sent along in the response from this endpoint.
Note that, with CORS, you generally only have to specify the origin. The routing system will automatically add in the appropriate HTTP verbs, and the authorization classes will add in the appropriate headers.
import clearskies
hello_world = clearskies.endpoints.Callable(
lambda: {"hello": "world"},
request_methods=["PATCH", "POST"],
authentication=clearskies.authentication.SecretBearer(environment_key="MY_SECRET"),
security_headers=[
clearskies.security_headers.Hsts(),
clearskies.security_headers.Cors(origin="https://example.com"),
],
)
wsgi = clearskies.contexts.WsgiRef(hello_world)
wsgi()
And then execute the options endpoint to see all the security headers:
$ curl -v http://localhost:8080 -X OPTIONS
* Host localhost:8080 was resolved.
< HTTP/1.0 200 Ok
< Server: WSGIServer/0.2 CPython/3.11.6
< ACCESS-CONTROL-ALLOW-METHODS: PATCH, POST
< ACCESS-CONTROL-ALLOW-HEADERS: Authorization
< ACCESS-CONTROL-MAX-AGE: 5
< ACCESS-CONTROL-ALLOW-ORIGIN: https://example.com
< STRICT-TRANSPORT-SECURITY: max-age=31536000 ;
< CONTENT-TYPE: application/json; charset=UTF-8
< Content-Length: 0
<
* Closing connection
internal_casing
Optional
Used in conjunction with external_casing to change the casing of the key names in the outputted JSON of the endpoint.
To use these, set internal_casing to the casing scheme used in your model, and then set external_casing to the casing scheme you want for your API endpoints. clearskies will then automatically convert all output key names accordingly. Note that for callables, this only works when you return a model and set readable_columns. If you set writeable_columns, it will also map the incoming data.
The allowed casing schemas are:
snake_casecamelCaseTitleCase
By default internal_casing and external_casing are both set to ‘snake_case’, which means that no conversion happens.
import clearskies
import datetime
class User(clearskies.Model):
id_column_name = "id"
backend = clearskies.backends.MemoryBackend()
id = clearskies.columns.Uuid()
name = clearskies.columns.String()
date_of_birth = clearskies.columns.Date()
send_user = clearskies.endpoints.Callable(
lambda users: users.create({"name":"Example","date_of_birth": datetime.datetime(2050, 1, 15)}),
readable_column_names=["name", "date_of_birth"],
internal_casing="snake_case",
external_casing="TitleCase",
model_class=User,
)
# because we're using name-based injection in our lambda callable (instead of type hinting) we have to explicitly
# add the user model to the dependency injection container
wsgi = clearskies.contexts.WsgiRef(send_user, classes=[User])
wsgi()
And then when called:
$ curl http://localhost:8080 | jq
{
"Status": "Success",
"Error": "",
"Data": {
"Name": "Example",
"DateOfBirth": "2050-01-15"
},
"Pagination": {},
"InputErrors": {}
}
external_casing
Optional
Used in conjunction with internal_casing to change the casing of the key names in the outputted JSON of the endpoint.
See the docs for internal_casing for more details and usage examples.
authentication
Optional
The authentication for this endpoint (default is public)
Use this to attach an instance of clearskies.authentication.Authentication to an endpoint, which enforces authentication. For more details, see the dedicated documentation section on authentication and authorization. By default, all endpoints are public.
authorization
Optional
The authorization rules for this endpoint
Use this to attach an instance of clearskies.authentication.Authorization to an endpoint, which enforces authorization. For more details, see the dedicated documentation section on authentication and authorization. By default, no authorization is enforced.