Quickstart - Integrate with Existing Codebase¶
In this quickstart, we will show how to integrate SpecStar into an existing FastAPI codebase.
SpecStar is designed for incremental adoption.
You can add it to your current system without rewriting your application.
Requirements¶
SpecStar requires:
- Python 3.11+
- FastAPI 0.115+
- Pydantic 1.10.26+ (v2 is also supported)
If your project is already using FastAPI, integration is typically straightforward.
When should you use this guide?¶
This guide is for you if:
- you already have a FastAPI service
- you don’t want to rewrite existing endpoints
- you want to gradually adopt SpecStar for new or existing resources
What we will do¶
In this guide, we will:
- keep an existing FastAPI app
- add one SpecStar-managed resource
- expose CRUD endpoints alongside existing APIs
- keep the original endpoints unchanged
1. Start from an existing FastAPI app¶
Assume you already have a FastAPI application:
from fastapi import FastAPI
app = FastAPI()
@app.get("/ping")
def ping():
return {"message": "pong"}
Your app is already running and serving endpoints.
2. Add a schema for the resource you want to manage¶
You can introduce SpecStar by defining a schema for one resource.
In this example, we add an Issue resource:
from datetime import datetime
from typing import Literal
import msgspec
class Issue(msgspec.Struct):
title: str
description: str | None = None
status: Literal["open", "in_progress", "resolved"] = "open"
severity: Literal["low", "medium", "high"] = "medium"
assignee: str | None = None
due_date: datetime | None = None
resolved_at: datetime | None = None
This schema can be completely independent from your existing routes and handlers.
3. Register the schema with SpecStar¶
Register the resource with SpecStar:
At this point, SpecStar knows about the resource, but it is not attached to your FastAPI app yet.
4. Apply SpecStar to your existing app¶
Now attach SpecStar to the existing FastAPI instance:
This will add CRUD routes for the registered resource.
Your existing endpoints are still there, and SpecStar adds new ones alongside them.
SpecStar only adds new routes and does not modify your existing endpoints.
Routes are generated directly from model names:
A minimal integrated example looks like this:
from datetime import datetime
from typing import Literal
import msgspec
from fastapi import FastAPI
from specstar import spec, Schema
from specstar.resource_manager import DiskStorageFactory
class Issue(msgspec.Struct):
title: str
description: str | None = None
status: Literal["open", "in_progress", "resolved"] = "open"
severity: Literal["low", "medium", "high"] = "medium"
assignee: str | None = None
due_date: datetime | None = None
resolved_at: datetime | None = None
app = FastAPI()
@app.get("/ping")
def ping():
return {"message": "pong"}
spec.configure(
storage_factory=DiskStorageFactory("./data")
)
spec.add_model(Schema(Issue, "v1"))
spec.apply(app)
Using APIRouter (recommended for existing projects)¶
If your application is structured with APIRouter, you can attach SpecStar to a router instead of the main app.
from fastapi import APIRouter, FastAPI
from specstar import spec, Schema
app = FastAPI()
router = APIRouter(prefix="/api")
@app.get("/ping")
def ping():
return {"message": "pong"}
spec.configure()
spec.add_model(Schema(Issue, "v1"))
spec.apply(app, router=router)
When router is provided, apply() automatically:
- Generates routes on the router
- Calls
app.include_router(router)to mount the router - Calls
openapi(app)to generate the OpenAPI schema
This allows you to:
- group SpecStar routes under a prefix (e.g.
/api/issue) - keep your existing routing structure unchanged
- integrate SpecStar without modifying your main app layout
If no router is provided, SpecStar will attach routes directly to the app.
If you need to include the router yourself (e.g. with custom tags or dependencies), pass auto_include=False:
spec.apply(app, router=router, auto_include=False)
app.include_router(router, tags=["my-custom-tag"])
spec.openapi(app)
5. Run the server¶
Start the development server:
Now your app serves both:
- your original endpoint:
/ping - SpecStar-generated endpoints:
/issueor/api/issue
You can also open:
- http://127.0.0.1:8000/docs
You should see both your existing routes and SpecStar-generated routes.
6. Use both systems together¶
Your original route still works:
Create an issue:
curl -X POST http://127.0.0.1:8000/issue \
-H "Content-Type: application/json" \
-d '{
"title": "Bug: API returns 500",
"status": "open",
"severity": "high",
"assignee": "alice"
}'
If using APIRouter:
curl -X POST http://127.0.0.1:8000/api/issue \
-H "Content-Type: application/json" \
-d '{
"title": "Bug: API returns 500",
"status": "open",
"severity": "high",
"assignee": "alice"
}'
List issues:
Or:
This shows that SpecStar can coexist with your current application instead of replacing it.
7. Add business rules incrementally¶
Once the resource is integrated, you can start adding application-specific logic.
For example:
titlemust not be empty- high-severity issues must have an assignee
- when an issue becomes
resolved, setresolved_atautomatically
from datetime import datetime, timezone
def validate_and_finalize_issue(new: Issue, old: Issue | None = None) -> Issue:
if not new.title.strip():
raise ValueError("title must not be empty")
if new.severity == "high" and not new.assignee:
raise ValueError("high severity issues must have an assignee")
if new.status == "resolved" and new.resolved_at is None:
new = Issue(
title=new.title,
description=new.description,
status=new.status,
severity=new.severity,
assignee=new.assignee,
due_date=new.due_date,
resolved_at=datetime.now(timezone.utc),
)
return new
To enable this rule, update the schema registration (validators are part of the schema):
8. Adopt SpecStar gradually¶
A common migration path:
- start with one resource
- keep existing endpoints unchanged
- let SpecStar manage new CRUD-heavy resources
- gradually move validation and lifecycle logic into schema-driven rules
You do not need to migrate your whole codebase at once.
9. Optional features¶
SpecStar also supports:
- generated Web UI
- job queue for background processing
See:
10. What’s next¶
From here, you can explore:
- adding more resources
- revision history and versioning
- custom lifecycle hooks
- custom routes
- background processing with job queue
A common next step is backend setup so your integrated resources persist correctly outside a demo environment.
Next Steps:
Appendix: Full example¶
# main.py
from datetime import datetime, timezone
from typing import Literal
import msgspec
from fastapi import APIRouter, FastAPI
from specstar import Schema, spec
class Issue(msgspec.Struct):
title: str
description: str | None = None
status: Literal["open", "in_progress", "resolved"] = "open"
severity: Literal["low", "medium", "high"] = "medium"
assignee: str | None = None
due_date: datetime | None = None
resolved_at: datetime | None = None
def validate_and_finalize_issue(new: Issue, old: Issue | None = None) -> Issue:
if not new.title.strip():
raise ValueError("title must not be empty")
if new.severity == "high" and not new.assignee:
raise ValueError("high severity issues must have an assignee")
if new.status == "resolved" and new.resolved_at is None:
new = Issue(
title=new.title,
description=new.description,
status=new.status,
severity=new.severity,
assignee=new.assignee,
due_date=new.due_date,
resolved_at=datetime.now(timezone.utc),
)
return new
app = FastAPI()
router = APIRouter(prefix="/api")
@app.get("/ping")
def ping():
return {"message": "pong"}
spec.configure()
spec.add_model(
Schema(Issue, "v1", validator=validate_and_finalize_issue),
)
spec.apply(app, router=router)