API Development¶
Keywords: rest api, json endpoints, api documentation, openapi swagger, automatic api docs, swagger ui
Asok provides tools for building robust JSON APIs with consistent response formats and automatic documentation.
Standard Responses¶
request.api(data, status=200)¶
Return a standardized success response.
from asok import Request
def render(request: Request):
users = User.all()
return request.api([u.to_dict() for u in users])
Response body:
{"data": [...], "status": 200}
request.api_error(message, status=400, errors=None)¶
Returns a standardized error response.
from asok import Request
def render(request):
if not request.form.get("email"):
return request.api_error("Validation failed", errors={"email": "required"})
Response body:
{"error": "Validation failed", "status": 400, "errors": {"email": "required"}}
API Security & CORS¶
When building an API, securing who can access your data is critical. Asok provides built-in mechanisms to handle Cross-Origin Resource Sharing (CORS) and rate limiting.
Rate Limiting¶
To prevent abuse or DoS attacks on your API endpoints, protect them using the @RateLimit decorator.
from asok import api, RateLimit
@api(summary="Fetch User Data")
@RateLimit(limit=60, window=60) # Max 60 requests per minute
def get(request, user_id: int):
return request.api({"id": user_id, "name": "John Doe"})
Internal Endpoints¶
If you are building an interactive UI (like an auto-completing search bar) that queries the server via AJAX, you might want to ensure the API endpoint is only accessible to your frontend application, and block external tools like Postman or scripts.
You can restrict access using the @internal_only decorator:
from asok import Request, api, internal_only
@api(summary="Realtime Search (Internal)")
@internal_only
def get(request: Request):
query = request.query.get("q", "")
# Your search logic here
return request.api({"results": []})
Note: For this to work, your frontend must send the X-Requested-With: XMLHttpRequest header when making the fetch request.
Auto-Generated Documentation¶
Asok automatically generates an OpenAPI 3.0 specification and a premium, interactive documentation interface for your project.
Docs UI:
/docs(by default)OpenAPI Spec:
/openapi.json
Disabling the Documentation¶
By default, the API documentation is automatically enabled whenever the application is running in DEBUG mode. If you need to explicitly disable the documentation (for example, in a production environment), you can do so in two ways:
1. Via Environment Variable (Recommended for Production)
Set ASOK_DOCS=false or DOCS=false when starting your server:
ASOK_DOCS=false asok run
2. Via Application Configuration
Explicitly set it to False in your configuration code:
app.config["DOCS"] = False
When disabled, the /docs and /openapi.json endpoints will return a 404 Not Found error.
Securing the OpenAPI Specification¶
By default, /openapi.json is public. You can restrict access to the OpenAPI specification endpoint by providing an authorization hook:
def require_admin(request):
return request.user and request.user.has_role("admin")
app.config["OPENAPI_AUTHORIZE"] = require_admin
The hook receives the current Request and must return True to allow access. Returning False produces a 403 Forbidden response.
Branding & Customization¶
You can customize the appearance of your API documentation via your application configuration or .env file. Asok will automatically fall back to global project settings (PROJECT_NAME, SITE_LOGO) if API-specific ones aren’t provided.
# In your app entry point (e.g., wsgi.py)
app.config["API_TITLE"] = "RTNC API"
app.config["API_DESCRIPTION"] = "API for RTNC application"
app.config["API_LOGO"] = "uploads/images/django.svg"
Key |
Description |
Default |
|---|---|---|
|
The title displayed in the dashboard and browser tab. |
|
|
Path to a custom logo (relative to static or full URL). |
|
|
Verbose description shown in the Introduction section. |
Generic Asok description |
|
The URL path where the documentation is served. |
|
|
The URL path where the raw JSON spec is served. |
|
|
Hook |
|
The “Try it out” Suite¶
The documentation includes a cohesive testing suite for every endpoint. It features:
Unified JSON Input: A single input field for all parameters. The engine intelligently maps your keys to Path variables (like
{id}), Query strings, or the Request Body.Auto-Fills: Clicking “Try it out” automatically generates a JSON example based on your input schema.
Smart Filtering: System fields like
id,created_at, andslugare automatically filtered out of examples to prevent clutter.Secure Testing: Built-in CSRF protection allows you to test
POST,PUT, andDELETErequests directly from the UI.
The @api Decorator¶
Use the @api decorator to add metadata to your endpoint handlers. This metadata is used to populate the reference.
from asok import Request, api
@api(
summary="Create Task",
description="Adds a new task to the user's list.",
tags=["Tasks"],
input=TaskSchema,
output=TaskSchema
)
def post(request: Request):
data = request.json_body
task = Task.create(**data)
return request.api(TaskSchema().dump(task), status=201)
Metadata Fields:¶
summary(orname): A short header for the operation.description: A detailed explanation (supports Markdown).tags: Used to group related operations together in the sidebar.input: A Schema class for request body generation and validation.output: A Schema class for response documentation.
Request Validation¶
When you provide an input schema to the @api decorator, Asok automatically validates the incoming request body. You can safely access the parsed data via request.json_body.
Model Serialization¶
For complex models, use the Schema class to control which fields are exposed and how they are formatted. This ensures your API responses are clean and consistent.
See the Serialization Guide for full details on building Schemas.
API Versioning¶
Asok provides native support for versioning your JSON APIs using three main strategies: URL versioning, request headers, and media type headers. It also automates sunset dates and deprecation announcements via standard HTTP response headers (RFC 8594).
Supported Versioning Strategies¶
URL Versioning: Path-based segmentation (e.g.,
/api/v1/usersand/api/v2/users). This resolves naturally via file-system routing when placed in versioned subdirectories (e.g.,src/pages/api/v1/users.py).Custom Header: Clients specify the version in the
X-API-Versionheader (e.g.,X-API-Version: v1).Accept Header: Clients specify the version in the
Acceptmedia type header (e.g.,Accept: application/vnd.asok.v2+json).
Unified Routing¶
If a client requests an unversioned URL (e.g., /api/users), the Asok router checks the request headers (X-API-Version or Accept). If a version header is found and a matching versioned file-system controller exists (such as src/pages/api/v1/users.py), the router automatically matches the requested version dynamically.
Deprecation & Sunset Headers¶
If an API version is deprecated, you can configure sunset dates. Asok will automatically append Deprecation: true and Sunset: <HTTP-date> response headers.
Module-level Configuration¶
To set metadata for an entire file-system controller:
# src/pages/api/v1/users.py
__api_deprecated__ = True
__api_sunset__ = "2026-12-31T00:00:00Z"
def get(request):
return request.json({"users": []})
Inline Routing & Decorators¶
You can manage versions inside a single controller file using versioned_response and the @api_version decorator:
# src/pages/api/users.py
from asok.api.versioning import api_version, versioned_response
@api_version(version="v1", deprecated=True, sunset="2026-12-31T00:00:00Z")
def get_v1(request):
return request.json({"users": ["Alice"]})
@api_version(version="v2")
def get_v2(request):
return request.json({"data": {"users": ["Alice"]}})
def get(request):
return versioned_response(request, {
"v1": get_v1,
"v2": get_v2,
}, default="v2")
Native GraphQL Server¶
Asok includes a native, zero-dependency GraphQL server. It automatically maps your ORM models to a GraphQL schema, validates query complexity, hosts the GraphiQL playground in development, and handles real-time subscriptions over WebSockets.
Activation & Configuration¶
To enable the GraphQL engine, set GRAPHQL_ENABLED = True in your application configuration (typically in wsgi.py):
from asok import Asok
app = Asok()
# 1. Enable the GraphQL endpoint (/graphql)
app.config["GRAPHQL_ENABLED"] = True
app.config["GRAPHQL_PATH"] = "/graphql" # Optional, default is /graphql
# 2. Query Complexity & Anti-DoS Protections
app.config["GRAPHQL_MAX_COMPLEXITY"] = 1000 # Protects against deep N+1 loops
app.config["GRAPHQL_MAX_DEPTH"] = 15 # Maximum nesting depth
# 3. Access Control (Recommended)
def require_admin(request):
return request.user is not None and getattr(request.user, "is_admin", False)
app.config["GRAPHQL_AUTHORIZE"] = require_admin
Note: In development mode (
DEBUG = True), visitingGET /graphqlin your browser opens the interactive GraphiQL Explorer to inspect the schema and test queries live. In production (DEBUG = False), onlyPOSTrequests are accepted.
Complete Practical Example¶
Let’s build a real-world example with an e-commerce catalog featuring categories and products.
1. Define ORM Models¶
Asok’s GraphQL engine inspects all registered ORM models and automatically generates types, queries, mutations, and subscriptions:
# src/models/catalog.py
from asok.orm import Model, Field, Relation
class Category(Model):
name = Field.String()
slug = Field.String()
# Relationship: One Category has many Products
products = Relation.HasMany("Product", foreign_key="category_id")
class Product(Model):
title = Field.String()
price = Field.Float()
stock = Field.Integer()
is_active = Field.Boolean(default=True)
category_id = Field.Integer()
# Relationship: Each Product belongs to a Category
category = Relation.BelongsTo("Category", foreign_key="category_id")
# Sensitive or internal fields
secret_vendor_code = Field.String(hidden=True) # Excluded from GraphQL schema
system_rating = Field.Float(protected=True) # Excluded from mutation arguments
2. Writing GraphQL Queries¶
A. List Query with Nested Relationships & Pagination¶
Query all products with their parent category name, limiting results to 5 items:
query GetProductsList {
products(limit: 5, offset: 0) {
id
title
price
stock
category {
id
name
slug
}
}
}
B. Single Record Query¶
Fetch a specific product by its unique ID:
query GetProductDetails {
product(id: 1) {
id
title
price
stock
category {
name
}
}
}
C. Using GraphQL Variables (Best Practice)¶
Avoid hardcoding parameters inside query strings. Use parameterized variables:
query GetProductById($id: Int!) {
product(id: $id) {
title
price
category {
name
}
}
}
With variable payload:
{
"id": 42
}
3. Writing GraphQL Mutations¶
Asok automatically generates create<Model>, update<Model>, and delete<Model> mutations.
A. Create a Record (createProduct)¶
mutation CreateNewProduct {
createProduct(
title: "Wireless Mechanical Keyboard",
price: 129.99,
stock: 25,
category_id: 2
) {
id
title
price
stock
}
}
B. Update a Record (updateProduct)¶
mutation UpdateProductStock {
updateProduct(id: 1, stock: 50, price: 119.99) {
id
stock
price
}
}
C. Delete a Record (deleteProduct)¶
mutation DeleteProduct {
deleteProduct(id: 1)
}
Returns true on successful deletion.
4. Real-time Subscriptions over WebSockets¶
When WebSocketServer is initialized, Asok automatically handles real-time subscriptions via the standard graphql-ws sub-protocol.
Every model exposes automatic hooks:
{model}Created— triggered wheneverModel.query().create()or a mutation runs.{model}Updated— triggered whenever a record is updated.{model}Deleted— triggered whenever a record is deleted.
subscription OnProductCreated {
productCreated {
id
title
price
category {
name
}
}
}
5. Consuming GraphQL from the Frontend (JavaScript / TypeScript)¶
Sending a GraphQL query from the browser using standard fetch():
// api/graphql.js
async function fetchGraphQL(query, variables = {}) {
const response = await fetch('/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify({
query: query,
variables: variables,
}),
});
const result = await response.json();
if (result.errors && result.errors.length > 0) {
console.error('GraphQL errors:', result.errors);
throw new Error(result.errors[0].message);
}
return result.data;
}
// Example usage:
const query = `
query GetCatalog($limit: Int!) {
products(limit: $limit) {
id
title
price
category {
name
}
}
}
`;
try {
const data = await fetchGraphQL(query, { limit: 10 });
console.log('Catalog products:', data.products);
} catch (err) {
console.error('Failed to load catalog:', err);
}
6. Executing with cURL¶
curl -X POST http://localhost:8000/graphql \
-H "Content-Type: application/json" \
-d '{
"query": "query { products(limit: 2) { id title price } }"
}'
Response:
{
"data": {
"products": [
{
"id": 1,
"title": "Wireless Mechanical Keyboard",
"price": 129.99
},
{
"id": 2,
"title": "Ergonomic Mouse",
"price": 59.99
}
]
}
}
Security & Access Control¶
2. Query Complexity Protection¶
To prevent Denial-of-Service attacks through deeply nested queries, Asok computes the complexity score before executing any query:
Scalar fields cost 1 point.
List fields multiply their children’s cost by their
limitargument (default 10).
app.config["GRAPHQL_MAX_COMPLEXITY"] = 250 # Default: 100
app.config["GRAPHQL_MAX_DEPTH"] = 10 # Default: 20
Configuration Reference¶
Key |
Description |
Default |
|---|---|---|
|
Master switch enabling the |
|
|
Path prefix for the GraphQL HTTP and WebSocket endpoint. |
|
|
Callable |
|
|
Allow unauthenticated read queries when no auth hook is defined. |
|
|
Allow unauthenticated mutations (development only). |
|
|
Maximum allowed query complexity score. Requests above this are rejected. |
|
|
Maximum allowed query nesting depth. |
|