Hooks & overridable methods

Three layers of customization: lightweight hooks for common cases, CRUD pieces when you need to change one operation, and full HTTP handlers when you need everything.

Layer 1 — Hooks (recommended)

Async methods that run at fixed points in dispatch. Default implementations are no-ops. Override them on your resource.

async def pre_process(self, request)

Called once per request, after authentication, before body parsing. Set self.queryset_filter, fetch related context, mutate self.allowed_methods based on user role.

async def pre_process(self, request):
    if self.user and self.user.get('role') != 'admin':
        self.queryset_filter = {'owner_id': self.user['id']}

async def before_cache(self, request)

Called only when cache=True, before the Redis cache lookup. Fold extra context into self.cache_key so the cache varies on it.

async def before_cache(self, request):
    role = self.user.get('role') if self.user else 'anon'
    self.cache_key += f':role={role}'

async def hydrate(self, body)

Mutate the parsed JSON body before the handler runs. Useful for normalizing input formats, computing derived fields, stripping invalid data.

async def hydrate(self, body):
    if 'email' in body:
        body['email'] = body['email'].lower().strip()

async def dehydrate(self, row)

Called for every row in the response. Strip secrets, add computed fields, rename keys, format dates.

async def dehydrate(self, row):
    row['display_name'] = row.get('name', '').title()
    row.pop('internal_token', None)
    if row.get('created_at'):
        row['created_at_human'] = row['created_at'].strftime('%Y-%m-%d')

async def alter_list(self, results)

Reshape the entire list before the meta envelope is added. Sort, dedupe, inject summary rows.

async def alter_list(self, results):
    return sorted(results, key=lambda r: r['priority'], reverse=True)

async def alter_detail(self, result)

Reshape the entire detail object before serialization.

async def alter_detail(self, result):
    result['permissions'] = await fetch_permissions(self.user, self.obj)
    return result

async def post_process(self, response)

Last chance to modify the response before JSON encoding. Returns the modified value.

async def post_process(self, response):
    if self.method == 'patch' and self.diff:
        await audit_log(self.user, self.diff)
    return response

async def add_m2m(self, result)

Hook for many-to-many relationships. Default is a no-op. BaseTagsResource and BaseCustomResource use it to attach tags / custom attributes.

Layer 2 — CRUD pieces

Override these when you want to keep dispatch but change one operation.

MethodRuns onWhen to override
get_objs(request)GET listCustom listing logic; still benefits from pagination/filters/cache
get_obj(id)GET detailCustom detail logic; pre-loaded select_related/prefetch
create_obj(request, body)POSTCustom create — keep cache invalidation by calling super() or invalidating yourself
update_obj(id, body)PATCHCustom update — useful when fields need a side effect
delete_obj(id)DELETESoft-delete, cascade rules, audit
count()?count=trueReplace InnoDB-optimized count

Listing mechanics

MethodEffect
build_filters(request)Applies filter_fields, search, tags, queryset_filter
get_filters(request)Applies ?filter= JSON / segments
paginate(request)Reads page and limit from query
ordenate(request)Reads ?order_by= and validates against order_fields

Response shaping

MethodEffect
return_results(results)Wraps list with {meta, objects}, applies normalize_list
return_result(result)Filters detail by edit_fields, applies normalize_obj
serialize(result)JSON-encodes with the resource's timezone, calls dehydrate/post_process/save_cache

Cache

MethodEffect
save_cache(content)Writes the response into Redis with TTL and namespace tracking
invalidate_cache(namespaces)Drops every key in the given namespaces
_cache_namespaces(include_detail_id=None)Builds the list of namespaces to invalidate (e.g. on PATCH)

Auth & security

MethodEffect
_ownership_filter()Returns extra filter kwargs when owner_field is set
_authenticate(request)Resolves the authenticated session (Bearer, X-Api-Key, or cookie — same precedence as _authenticate)
_enforce_token(request)Validates X-Token when ENFORCE_TOKEN
block(identifier)Adds the IP to the 24h block list
check_is_blocked(identifier)Short-circuits dispatch with 403 when the IP is blocked

Layer 3 — Full handlers

Override the HTTP verb method itself to take total control. You lose the surrounding plumbing (cache, validation, m2m save) — get it back by calling super() or replicating the relevant parts.

MethodWhen to override
get(request)Full custom behaviour for GET — list and detail combined
post(request)Full custom create flow
patch(request)Full custom update flow
delete(request)Full custom delete flow
dispatch(request, *args, **kwargs)Replace the entire pipeline — almost never needed
🪝

Prefer the highest layer that gets the job done. Hooks are cheap, CRUD pieces keep most of dispatch, full handlers throw it all away. Going one level too low is the most common over-engineering trap.

0-mcp by Stamatios Stamou Jr — github.com/ssjunior/0-mcp