Skip to content

Operations

An operation is a Python function bound to a URL path and one or more HTTP methods. You declare one by decorating a view function on a NinjaAPI instance (or a Router) with @api.get, @api.post, and friends.

Defining operations

Django Ninja has a decorator for each of the standard HTTP methods:

from ninja import NinjaAPI

api = NinjaAPI()


@api.get("/hello")
def get_hello(request):
    return {"message": "hello"}


@api.post("/hello")
def post_hello(request):
    return {"message": "hello"}


@api.put("/hello/{id}")
def put_hello(request, id: int):
    return {"message": "hello"}


@api.patch("/hello/{id}")
def patch_hello(request, id: int):
    return {"message": "hello"}


@api.delete("/hello/{id}")
def delete_hello(request, id: int):
    return {"message": "hello"}

The same decorators are available on a Router, so everything on this page applies whether you register operations directly on api or on a router mounted with add_router.

Handling multiple methods

To handle several methods with a single function, use api_operation:

from ninja import NinjaAPI

api = NinjaAPI()


@api.api_operation(["POST", "PATCH"], "/tasks/{task_id}")
def upsert_task(request, task_id: int):
    return {"task_id": task_id}

This is also how you implement methods that don't have their own shortcut decorator, such as HEAD or OPTIONS:

from ninja import NinjaAPI

api = NinjaAPI()


@api.api_operation(["HEAD", "OPTIONS"], "/tasks")
def tasks_meta(request):
    return {}

Sync vs async

Any operation can be declared with def or async def — Django Ninja detects this automatically and calls the view accordingly:

@api.get("/hello")
def hello(request):
    return {"message": "hello"}
@api.get("/hello")
async def hello(request):
    return {"message": "hello"}

You can freely mix sync and async operations on the same API or router. See Async Support for the details on running under ASGI and calling async code from your views.

Operation options

Every operation decorator (get, post, put, patch, delete, api_operation) accepts the same set of keyword-only arguments, on top of path:

Argument Type Default Description
response schema, or dict[int, schema] not set (returned value is serialized as-is) Validates and serializes the return value. See Responses.
operation_id str \| None auto-generated Unique OpenAPI operationId for this operation.
summary str \| None auto-generated from the function name Short, human-readable name shown in the docs UI.
description str \| None the function's docstring Longer explanation shown in the docs UI.
tags list[str] \| None inherited from the router Groups operations together in the docs UI.
deprecated bool \| None None (False) Marks the operation as deprecated in the schema and docs UI.
by_alias bool \| None None (False) Serialize response fields using their alias instead of their Python name.
exclude_unset bool \| None None (False) Omit response fields that were never explicitly set.
exclude_defaults bool \| None None (False) Omit response fields equal to their default value.
exclude_none bool \| None None (False) Omit response fields whose value is None.
url_name str \| None the view function's name Name used to reverse() this operation's URL. See URLs & Reverse.
include_in_schema bool True Excludes the operation from the OpenAPI schema (and docs UI) when False.
openapi_extra dict \| None None Extra keys merged into this operation's OpenAPI entry.

They also accept auth and throttle, which override the API/router defaults for that single operation — see Authentication and Throttling.

tags

@api.get("/hello/")
def hello(request, name: str):
    return {"hello": name}


@api.post("/orders/", tags=["orders"])
def create_order(request, order: str):
    return {"success": True}

Tools that render the schema may group operations by tag — Swagger UI, for example, uses them to build its collapsible sections:

Tags

Tags on a whole router

Instead of tagging every operation individually, tag them all at once, either on the Router itself or when mounting it:

from ninja import Router

router = Router(tags=["events"])

# or, override it for a particular mount:
api.add_router("/events/", router, tags=["events"])

An operation's own tags argument always takes priority over the router's. See Routers for more on router-level configuration.

summary

By default, the summary is generated by title-casing the function name:

@api.get("/hello/")
def hello(request, name: str):
    return {"hello": name}

Default summary

Pass summary to override it (handy for a nicer name, or a translation):

@api.get("/hello/", summary="Say Hello")
def hello(request, name: str):
    return {"hello": name}

Custom summary

description

Use description, or a plain docstring, to explain what the operation does:

@api.post("/orders/", description="Creates an order and updates stock")
def create_order(request, order: str):
    return {"success": True}

Description

A docstring is convenient for a longer, multi-line description — including simple Markdown, which the docs UI renders:

@api.post("/orders/")
def create_order(request, order: str):
    """
    To create an order please provide:
     - **first_name**
     - **last_name**
     - and **list of Items** *(product + amount)*
    """
    return {"success": True}

Description from docstring

operation_id

The OpenAPI operationId is an optional unique string identifying an operation — when set, it must be unique across the whole schema. By default, Django Ninja builds it from the view's module and function name.

Set it explicitly per operation:

@api.post("/tasks", operation_id="create_task")
def new_task(request):
    ...

Or override the naming logic for the whole API by subclassing NinjaAPI and overriding get_openapi_operation_id:

from ninja import NinjaAPI
from ninja.operation import Operation


class MySuperApi(NinjaAPI):
    def get_openapi_operation_id(self, operation: Operation) -> str:
        # operation gives you .path, .view_func, .methods, etc.
        return operation.view_func.__name__


api = MySuperApi()

deprecated

Mark an operation as deprecated without removing it:

@api.post("/make-order/", deprecated=True)
def some_old_method(request, order: str):
    return {"success": True}

It's flagged as deprecated in both the JSON schema and the interactive docs:

Deprecated

include_in_schema

Hide an operation from the OpenAPI schema (and therefore the docs UI) while keeping it fully functional — useful for internal or health-check endpoints:

@api.post("/hidden", include_in_schema=False)
def some_hidden_operation(request):
    pass

openapi_extra

For anything the other options don't cover, merge arbitrary keys straight into the operation's OpenAPI entry with openapi_extra. For example, to describe a request body that isn't backed by a schema:

@api.get(
    "/tasks",
    openapi_extra={
        "requestBody": {
            "content": {
                "application/json": {
                    "schema": {
                        "required": ["email"],
                        "type": "object",
                        "properties": {
                            "name": {"type": "string"},
                            "phone": {"type": "number"},
                            "email": {"type": "string"},
                        },
                    }
                }
            },
            "required": True,
        }
    },
)
def some_operation(request):
    pass

Or to document extra responses beyond the ones generated automatically:

@api.post(
    "/tasks",
    openapi_extra={
        "responses": {
            400: {"description": "Error Response"},
            404: {"description": "Not Found Response"},
        },
    },
)
def some_operation_2(request):
    pass

by_alias, exclude_unset, exclude_defaults, exclude_none

These four map directly onto the underlying Pydantic model_dump() call used to serialize your response, letting you tune the output per operation:

from ninja import Schema
from pydantic import Field


class UserOut(Schema):
    name: str = Field(alias="userName")
    nickname: str | None = None


@api.get("/users/{user_id}", response=UserOut, by_alias=True, exclude_none=True)
def get_user(request, user_id: int):
    return UserOut(userName="John", nickname=None)

Here the response is keyed by userName instead of name (by_alias=True), and nickname is dropped entirely since it's None (exclude_none=True).

All four default to False. They can also be set once on a Router(...) to apply to every operation registered on it, unless an operation overrides them explicitly.

Tip

These are output-serialization switches, not validation rules — they only affect what ends up in the response body. For everything else about shaping responses (multiple status codes, returning a plain HttpResponse, streaming, etc.), see Responses.