How to Handle URL Parameters in Mitsuki Controllers
A guide to reading path variables and query parameters with automatic type conversion.
Mitsuki
Mitsuki, is a web development framework, focused on bringing enterprise strength without the enterprise pain to Python.
It's opinionated, lightweight and performant.
Introduction
REST APIs usually involve reading dynamic data directly from the URL.
This data generally comes in two forms:
-
Path variables: which identify a specific resource (e.g.,
/posts/some_id) -
Query parameters: which are usually used for sorting or filtering collections (e.g.,
/posts?popular=true).
Mitsuki makes handling both of these trivial and type-safe, allowing you to define complex URL-based data retrieval with minimal code.
Setup
For the purposes of this guide, let's create a simple Post entity, a @CrudRepository to access its data, and a @Service to contain our business logic:
from dataclasses import dataclass
from typing import List, Optional
from mitsuki import Entity, Id, CrudRepository, Service
# 1. The Entity: Defines the Post data model
@Entity()
@dataclass
class Post:
id: int = Id()
title: str = ""
published: bool = True
# 2. The Repository: The data access layer for Posts
@CrudRepository(entity=Post)
class PostRepository:
# Gets standard CRUD methods automatically.
async def find_by_published(self, published: bool) -> List[Post]: ...
# For custom filtering with pagination, we use @Query
@Query("""
SELECT p FROM Post p
WHERE p.title LIKE :title_pattern AND p.published = :published
ORDER BY p.id DESC
""")
async def search_paginated(self, title_pattern: str, published: bool, limit: int, offset: int) -> List[Post]: ...
# 3. The Service: Contains business logic
@Service()
class PostService:
def __init__(self, repo: PostRepository):
# Mitsuki automatically injects the PostRepository here
self.repo = repo
async def get_by_id(self, post_id: int) -> Optional[Post]:
return await self.repo.find_by_id(post_id)
async def search(self, q: str, published: bool, page: int, size: int) -> List[Post]:
offset = page * size
return await self.repo.search_paginated(
title_pattern=f"%{q}%",
published=published,
limit=size,
offset=offset
)
Path Variables: Accessing Resource Identifiers
Path variables are used to capture parts of the URL path, most commonly for resource IDs. You define a path variable in your mapping decorator with curly braces {} and accept it as an argument in your controller method.
Let's create an endpoint to fetch a specific post by its ID:
from mitsuki import RestController, GetMapping
from typing import Optional
from ..services.post_service import PostService
from ..domain.post import Post
@RestController("/api/posts")
class PostController:
def __init__(self, post_service: PostService):
# The PostService is automatically injected by Mitsuki's DI container
self.post_service = post_service
@GetMapping("/{post_id}")
async def get_post_by_id(self, post_id: int) -> Optional[Post]:
print(f"Fetching post with ID: {post_id}")
print(f"Type of post_id is: {type(post_id)}")
post = await self.post_service.get_by_id(post_id)
return post
There are two key things happening here:
- Name Matching: Mitsuki automatically matches the
{post_id}in the path to thepost_idargument in the method. - Automatic Type Conversion: By type-hinting
post_id: int, you tell Mitsuki to convert the incoming path segment from a string to an integer. If a client requests/api/posts/abc, Mitsuki will automatically respond with a400 Bad Requesterror because "abc" cannot be converted to an integer.
Query Parameters: Filtering and Sorting
Query parameters are the key-value pairs that appear after the ? in a URL. They are ideal for optional filters, search queries, or pagination controls.
To read query parameters, you use the @QueryParam decorator on your method arguments. Let's add a search endpoint to our PostController.
# Continuing inside the PostController class...
from mitsuki import QueryParam
@GetMapping("/search")
async def search_posts(
self,
q: str = QueryParam(required=True),
published: bool = QueryParam(default=True),
page: int = QueryParam(default=0),
size: int = QueryParam(default=10)
) -> List[Post]:
# Example Request: /api/posts/search?q=mitsuki&published=true&page=0&size=20
#
# q: "mitsuki" (required string)
# published: True (boolean, defaults to True)
# page: 0 (integer, defaults to 0)
# size: 20 (integer, defaults to 10)
posts = await self.post_service.search(q, published, page, size)
return posts
Let's break down how @QueryParam works:
- Required Parameter:
q: str = QueryParam(required=True)explicitly marks theqparameter as mandatory. If a client makes a request without it, Mitsuki will automatically return a400 Bad Requesterror with a descriptive message. - Optional Parameter with Default:
published: bool = QueryParam(default=True)is optional. If thepublishedparameter is missing from the URL, its value will beTrue. The type hint ensures that values like"true","1", or"on"are correctly converted to the booleanTrue. - Optional Parameter defaulting to
None: If you were to define a parameter assize: Optional[int] = QueryParam(default=None), it would default toNoneif not provided. A plainQueryParam()also creates an optional parameter that defaults toNone.
Next Steps
You've seen how Mitsuki's declarative approach makes it easy to read and validate data from a URL. This type-safe mechanism helps prevent common bugs and cleans up your controller logic.
- Explore the docs: For a full list of available decorators and validation options, see the official Controllers & Request Handling documentation.
- Handle Request Bodies: Now that you can read data from a URL, learn how to handle data sent in the request body by reading our guide to
@RequestBody.
Happy coding! ❀
Top comments (0)