Introduction
FlaskNova is a productivity-first framework extension built for the modern Python era. It was designed with a single goal: to make API development in Flask as seamless and automated as possible.
Improving developers experience, FlaskNova brings modern features to the Flask ecosystem. By leveraging Pydantic for data validation, ensures your request payloads and response models are strictly typed, validated, and self-documenting.
Zero Migration Debt
No need to rewrite your entire legacy Flask app. Get route metadata features while keeping your existing architecture intact.
Ecosystem Friendly
Fully compatible with your favorite extensions like Flask-Login, Flask-Admin, and Flask-SQLAlchemy.
Development shouldn't involve manually updating JSON schemas or writing tedious validation logic for every route. FlaskNova handles the heavy lifting, providing automatic OpenAPI documentation, type-safe routing, and built-in serialization. It transforms Flask from a micro-framework into a robust, full-featured API engine without losing the simplicity that Flask developers love.
Installation
Install the core package via pip:
pip install flask-nova
Getting Started
Basics: Simple GET Request
Define your routes using the @app.get decorator. FlaskNova automatically populates your
documentation UI.
Docstring Support
If you don't provide a summary or description, FlaskNova intelligently
parses your docstring: The first line becomes the Summary, and the following lines
become the Description.
@app.get("/welcome")
def welcome():
"""Welcome User.
This text will show up as the long description in Swagger/Redoc.
"""
return {"message": "Welcome to FlaskNova!"}
Check Sample Code
Respecting Flask Internals
FlaskNova doesn't take away Flask's internals. You can still use make_response,
jsonify, Response and custom headers exactly as you do in standard Flask.
from flask import make_response, jsonify
@api.route("/custom", methods=["GET"])
def custom():
data = {"message": "Custom response"}
response = make_response(jsonify(data), 201)
response.headers['X-Custom-Header'] = 'Value'
return response
Request/Response Handling
FlaskNova infers request bodies and response models from annotations, so your routes stay compact while still generating schema and validation.
JSON body + response model
from flask_nova import status
from pydantic import BaseModel, Field
class User(BaseModel):
name: str = Field(..., max_length=5)
age: str = Field(..., max_length=5)
@app.post("/")
def home(user: User) -> tuple[User, int]: # return type
return user, 200
Form data parsing
The same model can be bound from form data using Form().
This is useful for HTML forms and multipart submissions. Use
application/x-www-form-urlencoded for ordinary fields and
multipart/form-data when files are included.
from typing import Annotated
from flask_nova import Form
@app.post("/", response_model=Home)
def home(user: Annotated[User, Form()]):
return {"message": "Welcome Home!"}
@app.post("/", response_model=Home)
def home(user: User = Form()):
return {"message": "Welcome Home!"}
File uploads
Use File("profile") with FileStorage to receive the uploaded field named
profile. The client must send a multipart/form-data request.
from flask_nova import File, FileStorage
@app.post("/")
def upload(profile: FileStorage = File("profile")) -> dict[str, str]:
return {"filename": profile.filename}
@app.post("/upload")
def upload_file(profile: Annotated[FileStorage, File("profile")]):
return {"filename": profile.filename}
curl -X POST http://127.0.0.1:5000/upload \
-F "profile=@avatar.png"
Response serialization
Use response_model when you want output validated and serialized
as a model schema.
from typing import Literal
@app.get("/author/<int:author_id>/books/<string:genre>/")
def author(author: Author, author_id: int, genre: str, year: int, month) -> tuple[Author, Literal[201]]:
return author, 201
Literal[201] over
int for the status value. int only says that some integer may be
returned; Literal[201] tells FlaskNova and its OpenAPI generator that this route
returns the specific success status 201 Created.
response_model and a native
response dispatcher, FlaskNova chooses the response_model path.
If you use a route response dispatcher or response model, do not return Flask response objects like
jsonify, make_response, or Response from that route.
Return model-like values directly
When a route returns a Pydantic model, dataclass, or custom class with to_dict(), let
FlaskNova serialize it. Do not wrap it in jsonify() or make_response()
unless you intentionally need a raw Flask response.
from pydantic import BaseModel
class Profile(BaseModel):
username: str
@app.get("/profile", response_model=Profile)
def profile():
return Profile(username="alice")
When raw Flask responses are useful
Use a native Response for downloads, streaming, static content, or custom headers that
do not belong to a model-based response.
from flask import Response
@app.get("/download")
def download():
return Response("hello", mimetype="text/plain")
Template serialization
render_template from flask_nova support template ctx obj serialization
from flask_nova import render_template
class DashboardResponse:
id: int
name: str
role: Literal["admin", "user"]
@api.get("/render", response_model=DashboardResponse)
def admin_dashboard():
user = {"name": "Mani", "role": "admin", "id": 12345}
return render_template("index.html", **user)
The user context object will be serialized before passing it to the template
{
"name": "Mani",
"role": "admin"
}
Query and path parameters
FlaskNova infers primitive query and path values from function parameters.
@api.get("/t", response_model=Home)
def query_req(id: int, name: str, age: int):
return {"message": f"Welcome Home! {id}-{name}-{age}"}
Path segments like <int:author_id> and query values are both
supported in the same handler.
support Field from pydantic for validation
Custom classes
to_dict() attribute. That attribute is used to identify the type,
not to automatically generate the response data for you.
class User2:
age: int | None
name: str
def to_dict(self):...
Colored JSON logging
Enable JSON log formatting with config:
app.config["ANSI_COLOR_JSON_LOG"] = True.
app.config["ANSI_COLOR_JSON_LOG"] = True
app.logger.error("/register", exc_info=True)
When this setting is enabled, app.logger emits colored JSON output
suitable for terminal consoles and structured logging.
from flask_nova import status
Why Pydantic?
Unlike standard Flask dictionaries, Pydantic offers:
- Automatic Validation: Invalid data is rejected with 422 errors before reaching your code.
- IDE Support: Full autocomplete and type hinting.
- Data Casting: Automatically converts "123" to
123for integer fields.
How request validators differ
FlaskNova supports Pydantic models, dataclasses, and custom classes for request binding. Each option has different validation behavior.
Pydantic
Invalid data is rejected before business logic runs. For example, an age value
such as "thirty" fails validation because the field expects an integer.
Extra fields are ignored by default. Missing required fields raise a validation error.
Dataclass
The request shape is strict: extra fields or missing fields raise a TypeError.
Dataclasses check the object structure, but they do not validate field types at runtime.
Custom class
FlaskNova uses the class attributes to build the request object. Every declared field is
expected explicitly, including optional fields; an omitted optional field is set to
None.
{
"username": "mani",
"age": "thirty"
}
With a Pydantic model such as age: int, this request fails during validation and never
reaches the route's business logic.
Create Your First App
FlaskNova is a Flask subclass, so a new application follows the workflow you already know. Define a model, attach it to a route, and return the model directly.
from pydantic import BaseModel
from flask_nova import FlaskNova, status
app = FlaskNova(__name__)
class User(BaseModel):
username: str
age: int
@app.post("/register", response_model=User)
def register(user: User):
return user, status.CREATED
if __name__ == "__main__":
app.run(debug=True)
The request body is validated before register runs. A valid request looks like this:
{
"username": "alice",
"age": 30
}
Organize Routes with Blueprints
NovaBlueprint groups related endpoints in a feature module while keeping Flask's
registration model. This is useful when an API grows beyond one file.
from flask_nova import FlaskNova, NovaBlueprint
app = FlaskNova(__name__)
api = NovaBlueprint("api")
@api.get("/health")
def health():
return {"status": "ok"}
app.register_blueprint(api)
Keep models, dependencies, services, and route groups separate so each handler stays focused on translating an HTTP request into an application operation.
Dependency Injection (Depend)
Depend( dependency=None)
Modularize your logic by injecting dependencies directly into your function signatures.
from flask_nova import Depend
def get_token_header(x_token: str = "secret"):
return x_token
@app.get("/secure")
def secure_route(token = Depend(get_token_header)):
return {"token": token}
Depend by nature give you scoped life time
Providers may be synchronous or asynchronous. FlaskNova resolves an async provider before calling the route, so both styles can be used for request-scoped authentication or service setup.
from flask_nova import Depend
async def get_current_user():
return {"username": "alice"}
@app.get("/me")
def me(user=Depend(get_current_user)):
return user
The @guard Decorator
Combine multiple security layers or filters into one clean decorator. Avoid: Decorator stacking which makes code hard to read.
from flask_jwt_extended import jwt_required
from flask_nova import guard
@app.get("/admin")
@guard(jwt_required(), admin_only_check)
def admin_dashboard():
return {"data": "confidential"}
Error Handling & Color Logging
FlaskNova uses a robust HTTPException class to handle errors consistently.
from flask_nova import HTTPException, status
raise HTTPException(
status_code=status.NOT_FOUND,
detail="User not found",
title="Resource Error"
)
from flask_nova import get_flasknova_logger
logger = get_flasknova_logger()
logger.info("FlaskNova app started!")
[201] POST /register - 0.045s
[ERROR] 422 Unprocessable Entity: "age" must be integer
Tracing (trace id & traceparent)
FlaskNova assigns a per-request trace id and exposes it on the Flask
request context as g.trace_id. This id can be used in
logs to correlate requests. Error responses produced by FlaskNova
include a W3C traceparent header so clients and
observability tools can correlate traces across services.
Task helpers: to_thread and to_process
For offloading work from request handlers, FlaskNova exposes two small helpers:
to_thread(func, ...): run sync or async callables on a worker thread from an async context while preserving the Flask application context. Useful for I/O-bound or blocking tasks.to_process(app, func, ...): run CPU-bound work in a process or thread pool (GIL-aware) while preserving the Flask application context. Prefer this for heavy CPU tasks.
# example: use to_thread in an async handler
result = await to_thread(some_io_bound_function, 1000)
# example: offload cpu work
result = await to_process(app, cpu_heavy_function, 4, arg1)
Set a limit around a long-running task with asyncio.wait_for:
import asyncio
try:
result = await asyncio.wait_for(
to_thread(long_running_task),
timeout=30.0,
)
except asyncio.TimeoutError:
print("Task exceeded 30 seconds")
to_process must be compatible with process-pool serialization. Keep process work
self-contained and pass simple, serializable values where possible.Ecosystem Integrations
Flask-SQLAlchemy (Full CRUD)
FlaskNova does not automatically serialize SQLAlchemy models. Convert the database model into a Pydantic model, dataclass, or custom class before returning it from a route.
from pydantic import BaseModel
from flask_nova import status
class UserResponse(BaseModel):
username: str
age: int
@app.post("/users", response_model=UserResponse)
def create_db_user(data: UserSchema) -> tuple[UserResponse, int]:
new_user = User(username=data.username, age=data.age)
db.session.add(new_user)
db.session.commit()
# Explicitly map the SQLAlchemy model to a FlaskNova response model.
response = UserResponse(
username=new_user.username,
age=new_user.age,
)
return response, status.CREATED
Flask-JWT-Extended
Handling Refresh Tokens and Access Tokens with ease using Depend.
@app.get("/me")
def get_me(user = Depend(current_user)):
return user
View JWT Integration Example
Configuration
FlaskNova currently exposes the following application configuration switch. The documentation routes
use the standard paths /docs, /redoc, /scalar, and
/openapi.json.
| Variable | Default | Description |
|---|---|---|
| FLASKNOVA_ENABLED_DOCS | True | Set to False to hide Swagger/Redoc/Scalar in production. |
| FLASKNOVA_SWAGGER_ROUTE | /docs | Path for Swagger UI. |
| FLASKNOVA_REDOC_ROUTE | /redoc | Path for ReDoc. |
| FLASKNOVA_SCALAR_ROUTE | /scalar | Path for Scalar. |
| ANSI_COLOR_JSON_LOG | False | Enable colored JSON log formatting for app.logger. |
OpenAPI and Route Metadata
FlaskNova builds the OpenAPI document from the route signatures, models, and metadata you provide.
The generated document is available at /openapi.json and is used by the interfaces at
/docs, /redoc, and /scalar.
Application-level metadata describes the API as a whole:
from flask_nova import FlaskNova
app = FlaskNova(
__name__,
version="1.0.0",
summary="User management API",
description="An API for managing users.",
contact={"name": "API team", "email": "api@example.com"},
external_docs={"description": "Project guide", "url": "https://example.com/guide"},
)
Route metadata describes an individual operation. Use it to make generated documentation useful to both people and client tooling.
@app.get(
"/users",
tags=["Users"],
summary="List users",
description="Return all visible users.",
deprecated=False,
response_model=list[UserResponse],
)
def list_users() -> list[UserResponse]:
return [UserResponse(username="alice", age=30)]
Project Structure
A small separation of responsibilities makes model-driven APIs easier to test and maintain.
project/
├── app/
│ ├── __init__.py
│ ├── models.py
│ ├── routes.py
│ ├── deps.py
│ ├── services.py
│ └── errors.py
├── requirements.txt
├── run.py
└── tests/
└── test_users.py
Use separate input and output models when their contracts differ. For example, a create request may accept a password while the response must never expose it.
from pydantic import BaseModel, Field
class CreateUserInput(BaseModel):
username: str = Field(min_length=3, max_length=40)
age: int = Field(ge=18)
class UserResponse(BaseModel):
username: str
age: int
is_active: bool = True
Testing and Debugging
Use Flask's test client to check validation, response structure, and error behavior without starting a server.
def test_create_user(client):
response = client.post(
"/users",
json={"username": "alice", "age": 30},
)
assert response.status_code == 200
assert response.json["username"] == "alice"
When a request fails, inspect the request model first, then the dependency layer, and finally the route logic. Validation happens before the route body, which makes failures easier to isolate.
Production Checklist
- Run behind a production WSGI server instead of using
app.run(debug=True). - Disable or protect interactive documentation when it should not be public.
- Keep secrets, credentials, and password hashes out of response models and logs.
- Use
HTTPExceptionfor predictable API errors and preserve the returned trace ID when reporting failures. - Use a task queue for durable, retryable background work; thread and process helpers are best for work that belongs to the current request lifecycle.
CLI Mastery
Flask-Nova provides a powerful CLI tool to automatically generate artifacts for your routes.
Command
Options
| Option | Description |
|---|---|
--app TEXT |
Required. App path, e.g. examples.form_ex:app |
--format [http|py|all] |
File format to generate (default: all) |
--base-url TEXT |
Base URL for requests (default: http://127.0.0.1:5000) |
--output PATH |
Directory to save files (default: current directory) |
[SCAN] 14 routes detected...
[DONE] Created 14 .http requests in ./docs/requests
[DONE] Created 14 pytest scripts in ./tests/gen
Command
The info command display's a browser summary of your app
Project Evolution
FlaskNova has moved fast, evolving from a core routing engine to a full-featured API framework in record time.
-
v0.1.0: The Foundation CORE
The birth of the Core Routing Engine. Launched with automatic OpenAPI schema generation, deep Pydantic integration for typed URL parameters, and customizable metadata support.
-
v0.1.1: Security & Logic ENHANCEMENT
Introduction of the
@guard()decorator for clean middleware handling andForm()parsing for complex payloads. Streamlined configuration by removingset_swagger()in favor of automated discovery. -
v0.1.2: Full API Spectrum ECOSYSTEM
Expanded route handling to support the full REST spectrum:
GET,POST,PATCH,PUT, andDELETE. Added ReDocs support alongside Swagger for beautiful, alternative documentation. -
v0.2.0: Optimized version introducting Zero overhead runtime. REFACTOR
Added scalar UI
Extended route metadata
Info cli add to display info about the app -
v0.2.1: Improve features and fixes. ENHANCEMENT
- File support
content_typefor explicit file request status_code,additionalOperations,externalDocsmeta added to route decorator- add blueprint name to tags
- suppout pydantic
Fieldobject for path and query parameter validation -
render_templatefromflask_novasupport response serialization
- File support