DEV Community

Cover image for How to Create Your First Mitsuki REST API from Scratch
David Landup
David Landup

Posted on

How to Create Your First Mitsuki REST API from Scratch

A step-by-step guide to using the mitsuki init CLI, understanding the generated project, and running your first high-performance web server.

Introduction

Mitsuki, is a web development framework, focused on bringing enterprise strength without the enterprise pain to Python.

It's opinionated, lightweight and performant.

Bootstrapping Mitsuki Projects

When you get a spark of inspiration, the first thing that comes to mind isn't boilerplate setup. It's standard, usually the same, and you might even have a project starter skeleton sitting on your device already.

When starting something new - I want to focus on the system design, business logic and value proposition. Thus, I heavily use my own templates.

Let's go from an empty folder to a fully functional, high-performance API in a single minute!

Step 1: Install Mitsuki

Mitsuki is available on PyPI. Installing it will also install the CLI tool:

$ pip install mitsuki
Enter fullscreen mode Exit fullscreen mode

Step 2: mitsuki init 🪄

Let's use the built-in CLI to scaffold a complete project structure.
Run the following command in your terminal:

$ mitsuki init
Enter fullscreen mode Exit fullscreen mode

The CLI will launch an interactive wizard to guide you. Let's create a simple API for a blog:

$ mitsuki init
Application name: blog_api
Description (optional): API for my new blog
Database type [sqlite/postgresql/mysql] (sqlite):
Create starter domain? [Y/n]: y
Domain name (e.g., User, Product): Post
Add another domain? [y/N]: n

Successfully created Mitsuki application: blog_api
Enter fullscreen mode Exit fullscreen mode

You can immediately spin it up and fire requests to it:

python3 -m blog_api.src.app
Enter fullscreen mode Exit fullscreen mode

Starts the server:

2025-11-29 10:30:15,132 - mitsuki - INFO     -
2025-11-29 10:30:15,132 - mitsuki - INFO     -     ♡ 。 ₊°༺❤︎༻°₊ 。 ♡
2025-11-29 10:30:15,132 - mitsuki - INFO     -               _ __             __   _
2025-11-29 10:30:15,132 - mitsuki - INFO     -    ____ ___  (_) /________  __/ /__(_)
2025-11-29 10:30:15,132 - mitsuki - INFO     -   / __ `__ \/ / __/ ___/ / / / //_/ /
2025-11-29 10:30:15,132 - mitsuki - INFO     -  / / / / / / / /_(__  ) /_/ / ,< / /
2025-11-29 10:30:15,132 - mitsuki - INFO     - /_/ /_/ /_/_/\__/____/\__,_/_/|_/_/
2025-11-29 10:30:15,132 - mitsuki - INFO     -     °❀˖ ° °❀⋆.ೃ࿔*:・  ° ❀˖°
2025-11-29 10:30:15,132 - mitsuki - INFO     -
2025-11-29 10:30:15,132 - mitsuki - INFO     - :: Mitsuki ::                (v0.1.3)
2025-11-29 10:30:15,132 - mitsuki - INFO     -
2025-11-29 10:30:15,133 - _granian - INFO     - Starting granian...
2025-11-29 10:30:15,139 - _granian - INFO     - Listening at: http://127.0.0.1:8000
Enter fullscreen mode Exit fullscreen mode

And you can just send requests to it:

curl -X POST http://localhost:8000/api/post \
-H "Content-Type: application/json" \
-d '{}'
Enter fullscreen mode Exit fullscreen mode

The API will respond with the newly created post, complete with a uuid, created_at, and updated_at timestamp:

{
  "id": "018ecb71-d556-7000-8000-000000000001",
  "created_at": "2025-11-29T10:35:00.123456",
  "updated_at": "2025-11-29T10:35:00.123456"
}
Enter fullscreen mode Exit fullscreen mode

However - it's worth taking a look at what's inside first, customizing your Post domain object, and understanding the structure first.

Step 3: Anatomy of a Mitsuki Project

While you can really have any structure you want in a Mitsuki project, a sensible default structure is baked into the scaffold.

Let's pop the hood and see what mitsuki init created for us inside the blog_api directory:

blog_api/
  blog_api/
    src/
      controller/       # 🚪 The front door for your API requests
      domain/           # 🏗️ The blueprint for your data (Post)
      repository/       # 🗄️ The bridge that talk to your database
      service/          # 🧠 The brain where your business logic lives
      app.py            # ❤️ The heart of your application
      __init__.py
    __init__.py
  application.yml       # ⚙️ The main control panel for settings
  application-dev.yml   # 🛠️ Settings for your development environment
  application-prod.yml  # 🚀 Settings for production
  .gitignore
  README.md
Enter fullscreen mode Exit fullscreen mode

What's Inside? Fully Functional API

This isn't just a collection of empty folders; it's a fully wired application.

The Post domain you specified in the beginning was created, with a corresponding RestController, Service, and CrudRepository -giving you a working CRUD API right out of the box.

Your PostController controller imports your PostService and exposes endpoints to create posts, return existing ones, and delete them:

import uuid
from typing import List
from mitsuki import RestController, GetMapping, PostMapping, DeleteMapping, QueryParam
from ..service.post_service import PostService


@RestController("/api/posts")
class PostController:
    """REST API controller for Post resources."""

    def __init__(self, service: PostService):
        self.service = service

    @GetMapping("")
    async def list_all(
        self,
        page: int = QueryParam(default=0),
        size: int = QueryParam(default=100)
    ):
        """GET /api/posts?page=0&size=100"""
        return await self.service.get_all(page=page, size=size)

    @GetMapping("/{id}")
    async def get_by_id(self, id: str):
        """GET /api/posts/123e4567-e89b-12d3-a456-426614174000"""
        entity = await self.service.get_by_id(uuid.UUID(id))
        if not entity:
            return {"error": "Not found"}, 404
        return entity

    @PostMapping("")
    async def create(self, body: dict):
        """POST /api/posts"""
        entity = await self.service.create()
        return entity, 201

    @DeleteMapping("/{id}")
    async def delete(self, id: str):
        """DELETE /api/posts/123e4567-e89b-12d3-a456-426614174000"""
        deleted = await self.service.delete(uuid.UUID(id))
        if not deleted:
            return {"error": "Not found"}, 404
        return {"success": True}

Enter fullscreen mode Exit fullscreen mode

Your Post @Entity, contains only the ID, created_at and updated_at fields. Don't forget to add other fields you want here, like a body, or title:

import uuid
from dataclasses import dataclass
from datetime import datetime
from mitsuki import Entity, UUIDv7, Field


@Entity()
@dataclass
class Post:
    """Post domain object."""
    id: uuid.UUID = UUIDv7()
    created_at: datetime = Field(update_on_create=True)
    updated_at: datetime = Field(update_on_save=True)

Enter fullscreen mode Exit fullscreen mode

Your PostRepository contains all the CRUD implementations by default in Mitsuki, as well as pointers for Query DSL, and custom query methods:

from typing import Optional, List
from mitsuki import CrudRepository, Query, Modifying
from ..domain.post import Post


@CrudRepository(entity=Post)
class PostRepository:
    """
    Repository for Post entities.

    Auto-implemented CRUD methods (you can just call these as-is):
    - save(entity) - Create or update
    - find_by_id(id) - Find by primary key
    - find_all(page, size, sort_by, sort_desc) - Paginated query
    - delete(entity) - Delete entity
    - delete_by_id(id) - Delete by ID
    - count() - Count all entities
    - exists_by_id(id) - Check existence

    Auto-generated query methods (define signature only):
    Examples:
        async def find_by_name(self, name: str) -> Optional[Post]: ...
        async def find_by_status(self, status: str) -> List[Post]: ...
        async def find_by_created_at_greater_than(self, created_at) -> List[Post]: ...

    Custom @Query methods:
        @Query("SELECT e FROM Post e WHERE e.status = :status ORDER BY e.created_at DESC")
        async def find_active_sorted(self, status: str) -> List[Post]: ...

    Custom @Modifying queries (UPDATE/DELETE):
        @Modifying
        @Query("UPDATE Post SET status = :status WHERE id = :id")
        async def update_status(self, id: int, status: str) -> int: ...

    Custom methods with direct database access:
        async def custom_complex_query(self, param: str) -> List[dict]:
            async with self.connection as conn:
                result = await conn.execute("SELECT * FROM table WHERE field = :param", {"param": param})
                return [dict(row) for row in result]
    """

    # Add your custom query methods here
    pass

Enter fullscreen mode Exit fullscreen mode

And your PostService, where your business logic lives, currently just wraps the repository for database operations. Add your business-specific logic here:

import uuid
from typing import List, Optional
from mitsuki import Service
from ..domain.post import Post
from ..repository.post_repository import PostRepository


@Service()
class PostService:
    """Service layer for Post business logic."""

    def __init__(self, repo: PostRepository):
        self.repo = repo

    async def get_all(self, page: int = 0, size: int = 100) -> List[Post]:
        """Get all posts with pagination"""
        return await self.repo.find_all(page=page, size=size)

    async def get_by_id(self, id: uuid.UUID) -> Optional[Post]:
        """Get post by ID"""
        return await self.repo.find_by_id(id)

    async def create(self) -> Post:
        """Create new post"""
        entity = Post(id=None)
        return await self.repo.save(entity)

    async def delete(self, id: uuid.UUID) -> bool:
        """Delete post by ID"""
        exists = await self.repo.exists_by_id(id)
        if not exists:
            return False
        await self.repo.delete_by_id(id)
        return True

Enter fullscreen mode Exit fullscreen mode

Step 4: Liftoff! Running Your Server 🚀

Time to spin up the server:

cd blog_api
python3 -m blog_api.src.app
Enter fullscreen mode Exit fullscreen mode

You'll be greeted by the Mitsuki startup log:

2025-11-29 10:30:15,132 - mitsuki - INFO     -
2025-11-29 10:30:15,132 - mitsuki - INFO     -     ♡ 。 ₊°༺❤︎༻°₊ 。 ♡
2025-11-29 10:30:15,132 - mitsuki - INFO     -               _ __             __   _
2025-11-29 10:30:15,132 - mitsuki - INFO     -    ____ ___  (_) /________  __/ /__(_)
2025-11-29 10:30:15,132 - mitsuki - INFO     -   / __ `__ \/ / __/ ___/ / / / //_/ /
2025-11-29 10:30:15,132 - mitsuki - INFO     -  / / / / / / / /_(__  ) /_/ / ,< / /
2025-11-29 10:30:15,132 - mitsuki - INFO     - /_/ /_/ /_/_/\__/____/\__,_/_/|_/_/
2025-11-29 10:30:15,132 - mitsuki - INFO     -     °❀˖ ° °❀⋆.ೃ࿔*:・  ° ❀˖°
2025-11-29 10:30:15,132 - mitsuki - INFO     -
2025-11-29 10:30:15,132 - mitsuki - INFO     - :: Mitsuki ::                (v0.1.3)
2025-11-29 10:30:15,132 - mitsuki - INFO     -
2025-11-29 10:30:15,133 - _granian - INFO     - Starting granian...
2025-11-29 10:30:15,139 - _granian - INFO     - Listening at: http://127.0.0.1:8000
Enter fullscreen mode Exit fullscreen mode

Step 5: See Your API in Action

Mitsuki automatically generates OpenAPI documentation for your API. No work needed.

Open your browser and navigate to http://localhost:8000/docs:

You'll find an interactive API documentation page where you can see all your endpoints and even test them directly from the browser.

Either through the terminal, or through the API UI, try creating a new post:

curl -X POST http://localhost:8000/api/post \
-H "Content-Type: application/json" \
-d '{}'
Enter fullscreen mode Exit fullscreen mode

The API will respond with the newly created post:

{
  "id": "018ecb71-d556-7000-8000-000000000001",
  "created_at": "2025-11-29T10:35:00.123456",
  "updated_at": "2025-11-29T10:35:00.123456"
}
Enter fullscreen mode Exit fullscreen mode

Your Journey Has Just Begun

In just a minute, you've bootstrapped a complete API with Mitsuki.

Not a "Hello World" app, you have a functional application starter with a database model, a REST API, and interactive documentation.

This is the benefit of convention over configuration. Mitsuki lets you focus on what truly matters: your business logic, and the value you want to deliver.

Next Steps

Ready to learn more?

  • Dive into the Official Documentation to explore custom queries, dependency injection, and more.
  • Add new fields to your Post entity in src/domain/post.py.
  • Implement unique business logic in your PostService at src/service/post_service.py.

Happy coding! ❀

Top comments (0)