How get relates to get_obj, post to create_obj, …

Every HTTP verb in BaseResource is implemented as a thin "handler" that orchestrates a "piece" — the function that does the actual work. Knowing which one to override saves you from accidentally rewriting cache invalidation, response wrapping or ownership checks.

Two layers, one verb

Each HTTP method has two methods on the resource:

HTTP verbHandler (orchestrates)CRUD piece (does the work)
GETget(request)get_obj(id) for detail, get_objs(request) for list
POSTpost(request)create_obj(request, body)
PATCHpatch(request)update_obj(id, body)
DELETEdelete(request)delete_obj(id)

What the handler does (and what the piece does not)

The handler:

The piece:

GET — the special one

GET is the only handler that can route to two different pieces depending on the URL:

async def get(self, request):
    if self.id:
        data = await self.get_obj(self.id)        # detail
        data = await self.alter_detail(data)
        ...
    else:
        data = await self._get_objs(request)      # list
        ...

Internally _get_objs calls get_objs then return_results (which wraps with meta + paginates). When you override get_objs you keep the wrap; when you override get you replace everything.

Pick the lowest layer that does the job

I want to…Override
Strip a sensitive column from every rowdehydrate(row)
Lower-case incoming emailshydrate(body)
Add a computed column to the list responseget_objs (call super())
Send a welcome email after signupcreate_obj (call super() first, then mail)
Forbid an update unless user is ownerupdate_obj (raise HTTPException before super())
Soft-delete instead of real DELETEdelete_obj (don't call super() — replace it)
Return raw bytes instead of JSONget / post / etc. (replace the handler)

Examples

Add a computed field — get_objs + super()

async def get_objs(self, request):
    rows = await super().get_objs(request)
    for row in rows:
        row['display_name'] = row.get('name', '').title()
    return rows

super() runs the standard query (filters, pagination, ordering, related fields, prefetch). You add the computed column on top.

Side effect after create — create_obj + super()

async def create_obj(self, request, body):
    result = await super().create_obj(request, body)
    await send_welcome_email(self.obj)
    return result

self.obj is set by super().create_obj after the row is created — so the side effect runs after persistence.

Soft-delete — delete_obj without super()

async def delete_obj(self, id):
    obj = await self.queryset.aget(pk=id)
    obj.deleted = True
    await obj.asave()
    return {'success': True, 'id': id, 'message': 'Soft-deleted'}

Here we don't call super() because we don't want the row removed. The handler still calls invalidate_cache afterwards — your soft-delete still drops the cache for free.

Replace the GET response entirely — get

async def get(self, request):
    if self.id:
        return HttpResponse(b'<binary>', content_type='application/octet-stream')
    return await super().get(request)

When the response is not JSON-shaped, replacing the whole get is cleaner than fighting serialize.

🪜

Rule of thumb: if you can do it from super(), do. The plumbing around the piece — cache invalidation, ownership checks, response wrapping — is what you actually want, not what you want to rewrite.

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