Best Python Libraries for Building REST APIs Without a Framework in 2026
If you've ever felt like Django or Flask were overkill for a small microservice or a weekend project, you're not alone. Sometimes you just want to spin up a REST API without pulling in hundreds of dependencies, a routing system you'll never fully use, or an opinionated project structure that takes longer to configure than to actually build.
The good news? Python's ecosystem in 2026 is rich with lightweight, purpose-built libraries that let you construct clean, production-ready REST APIs without touching a traditional framework at all. Whether you're building a high-performance microservice, an internal tool, or experimenting with serverless architectures, these libraries give you surgical control over every layer of your stack.
This post covers the best Python libraries for building REST APIs from scratch — the ones actually worth your time, with honest assessments of where each one shines and where it falls short.
Why Skip the Framework Altogether?
Before diving in, it's worth asking why you'd avoid frameworks in the first place.
Frameworks like FastAPI, Flask, and Django REST Framework are genuinely excellent. But they make decisions for you. When you're building a tightly scoped service — say, a webhook processor, a data pipeline endpoint, or a simple CRUD interface over a single database table — you often don't need routing middleware, template engines, session management, or the rest of the bundled ecosystem.
Building without a framework means:
- Smaller dependency footprint — fewer security vulnerabilities to track
- Faster cold starts — critical for Lambda or Cloud Run deployments
- Deeper understanding — you understand every line of your stack
- More flexibility — compose exactly what you need, nothing more
That said, this approach isn't for every project. If you're building a complex application with many endpoints, authentication flows, and team members onboarding regularly, a framework pays for itself. But for focused, lean APIs? Read on.
The Core Building Blocks You'll Actually Need
When building an API without a framework, you're essentially assembling a few key pieces yourself:
- An HTTP server / ASGI or WSGI handler
- Request parsing and routing logic
- Serialization and validation
- Response formatting
The libraries below cover these categories. Some overlap — and that's intentional. You'll likely combine two or three of them rather than relying on any single one.
HTTP Servers and Request Handling
httpx + Raw ASGI with uvicorn
uvicorn is one of the fastest ASGI servers available and serves as the runtime backbone for many Python APIs even when you strip everything else away. Pairing it with a raw ASGI application — a simple Python callable that accepts scope, receive, and send — gives you a working HTTP server with zero framework overhead.
async def app(scope, receive, send):
if scope["type"] == "http":
await send({
"type": "http.response.start",
"status": 200,
"headers": [[b"content-type", b"application/json"]],
})
await send({
"type": "http.response.body",
"body": b'{"message": "hello world"}',
})
This is as raw as it gets. You'd typically add a thin routing layer on top rather than doing path matching by hand, but the point is that uvicorn itself is production-grade and blazing fast.
Best for: Developers who want total control and are comfortable wiring up their own request dispatching.
Starlette (Without the Framework Label)
Here's the thing — Starlette is technically a toolkit and a lightweight framework, but it's so modular that you can use just its Request, Response, Router, and middleware components without buying into any larger structure. Most people know Starlette as "the thing FastAPI is built on," but it stands completely on its own.
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
async def homepage(request):
return JSONResponse({"status": "ok"})
app = Starlette(routes=[Route("/", homepage)])
You can go even more minimal by using just Router and Request without instantiating Starlette at all. The routing, middleware, and WebSocket support are all independently importable.
Best for: Projects that need solid routing and request handling without the weight of a full framework. If you later decide to add more structure, Starlette scales gracefully.
Validation and Serialization
Pydantic v2
If there's one library that has genuinely changed how Python developers think about data validation, it's Pydantic. In its v2 form (now the standard), it's built on Rust extensions and is dramatically faster than its predecessor.
You don't need any framework to use Pydantic. Define a model, pass raw dict data from your parsed request body, and get back a validated, typed object — or a clear error explaining exactly what went wrong.
from pydantic import BaseModel, EmailStr, ValidationError
class UserCreate(BaseModel):
username: str
email: EmailStr
age: int
try:
user = UserCreate(**request_body)
except ValidationError as e:
return {"errors": e.errors()}
Pydantic handles nested models, custom validators, field aliases, and even JSON schema generation for documentation. It's arguably the most important single library in this list.
Best for: Any project that receives structured data from clients — which is basically every API.
📘 Level up your Pydantic skills: Pydantic: Simplify Data Validation in Python on Real Python is one of the best structured courses for getting past the basics quickly.
msgspec
msgspec is a relative newcomer that has turned heads for its extraordinary performance. It handles both serialization and validation in a single pass, making it significantly faster than combining json + pydantic in tight loops.
import msgspec
class Order(msgspec.Struct):
item: str
quantity: int
price: float
order = msgspec.json.decode(b'{"item":"widget","quantity":2,"price":9.99}', type=Order)
If you're building a high-throughput API — think thousands of requests per second with complex payloads — msgspec is worth benchmarking seriously. It may not have Pydantic's ecosystem integrations, but for pure speed, it's exceptional.
Best for: Performance-critical APIs where serialization is a measurable bottleneck.
Routing Without a Framework
routes (WebOb-compatible Routing)
The routes library is a mature URL dispatch system inspired by Ruby on Rails' routing. It doesn't care what HTTP framework you're using — you feed it a path and it tells you which handler to call.
It's not the flashiest option, but it's battle-tested and handles complex URL patterns including named groups, conditions, and sub-domain routing.
Best for: Developers coming from other languages who prefer the Rails-style routing mental model.
Manual Routing with re and Dispatch Tables
For APIs with a small number of endpoints (under ~20), there's a strong argument for just writing a dispatch table yourself using Python's standard re module. No extra dependencies, easy to read, trivially debuggable.
import re, json
ROUTES = [
(re.compile(r"^/users/(?P<id>\d+)$"), "GET", get_user),
(re.compile(r"^/users$"), "POST", create_user),
]
def dispatch(path, method):
for pattern, allowed_method, handler in ROUTES:
match = pattern.match(path)
if match and method == allowed_method:
return handler, match.groupdict()
return None, {}
This is genuinely the right choice more often than developers admit. It's readable, zero-dependency, and completely predictable.
Request Parsing
multipart and python-multipart
When your API accepts file uploads or form data, you'll need a multipart parser. python-multipart is the standard choice and is what Starlette uses under the hood. It handles streaming multipart bodies cleanly without loading everything into memory.
For JSON bodies specifically, Python's built-in json module is perfectly adequate for most use cases — just json.loads(body) and move on.
orjson
orjson is a drop-in replacement for Python's standard json module that is dramatically faster and handles types like datetime, UUID, and numpy arrays natively.
import orjson
data = orjson.loads(request_body)
response_bytes = orjson.dumps({"result": data, "timestamp": datetime.utcnow()})
The datetime serialization alone makes it worth using — no more "Object of type datetime is not JSON serializable" errors at 2am.
Best for: Any API that serializes complex Python types or handles high request volume.
💡 Tip:
orjsonreturnsbytesrather thanstr, which is actually what you want when writing HTTP response bodies directly.
Authentication Without a Framework
PyJWT
PyJWT is the go-to for JSON Web Token handling in Python. Encoding and decoding JWTs, verifying signatures, and checking claims are all covered cleanly.
import jwt
token = jwt.encode({"user_id": 42, "exp": expiry}, SECRET_KEY, algorithm="HS256")
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
Without a framework handling authentication middleware, you'll wire this into your request dispatching manually — typically as a decorator or a simple function called at the top of your handler. It's a few extra lines, but nothing complex.
Putting It Together: A Minimal Example
Here's what a complete, production-ish microservice might look like combining Starlette's routing, Pydantic validation, orjson serialization, and PyJWT authentication:
from starlette.applications import Starlette
from starlette.responses import Response
from starlette.routing import Route
from pydantic import BaseModel, ValidationError
import orjson, jwt
SECRET = "your-secret-key"
class ItemCreate(BaseModel):
name: str
price: float
def json_response(data, status=200):
return Response(orjson.dumps(data), status_code=status,
media_type="application/json")
async def create_item(request):
auth = request.headers.get("Authorization", "")
try:
jwt.decode(auth.replace("Bearer ", ""), SECRET, algorithms=["HS256"])
except jwt.InvalidTokenError:
return json_response({"error": "unauthorized"}, 401)
body = await request.body()
try:
item = ItemCreate(**orjson.loads(body))
except ValidationError as e:
return json_response({"errors": e.errors()}, 422)
# ... save to database
return json_response({"created": item.model_dump()}, 201)
app = Starlette(routes=[Route("/items", create_item, methods=["POST"])])
Less than 30 lines. Handles auth, validation, serialization, and error responses. No framework required.
What About Documentation?
One thing frameworks give you for free is auto-generated docs (Swagger UI, ReDoc, etc.). Without a framework, you'll need to either:
- Write an OpenAPI spec manually using openapi-spec-validator and serve it statically
-
Use
spectree— a library that generates OpenAPI specs from Pydantic models and can slot into Starlette or even raw WSGI apps without a full framework - Accept that internal APIs sometimes don't need public docs
For most microservices, option 3 is underrated. Document your endpoints in a README and move on.
Deployment Considerations
A framework-free API is typically deployed the same way as any other Python async application:
-
Containerized: Docker +
uvicorn main:app --host 0.0.0.0 --port 8000 - Serverless: AWS Lambda with Mangum as an ASGI adapter — works perfectly with raw Starlette apps
-
Process management:
gunicornwithuvicornworkers for multi-process production deployments
The lightweight nature of these apps makes cold starts minimal, which is particularly valuable in serverless environments where billing starts at invocation time.
Quick Reference: Which Library for What
| Need | Library |
|---|---|
| ASGI server | uvicorn |
| Routing + request handling |
starlette (modular) |
| Data validation |
pydantic v2 |
| High-performance serialization |
orjson or msgspec
|
| JWT authentication | PyJWT |
| File/form uploads | python-multipart |
| URL routing (standalone) |
routes or custom re dispatch |
Final Thoughts
Building REST APIs without a framework isn't about being contrarian or proving you don't need abstractions. It's about choosing the right level of abstraction for the task at hand. A focused microservice with three endpoints doesn't need the same infrastructure as a full-stack web application.
The libraries covered here — uvicorn, starlette components, pydantic, orjson, and PyJWT — are all actively maintained, battle-tested in production at scale, and genuinely complement each other. You can assemble a fast, type-safe, well-validated API in under an hour with nothing you don't understand.
Start Building Today
Pick one of the patterns above and build something small this week. A webhook handler, a simple CRUD API for a side project, or a data transformation endpoint. The best way to understand what you actually need from a framework is to build without one at least once.
Have a library that belongs on this list? Drop it in the comments — this ecosystem moves fast and community recommendations are always welcome.
If you found this post useful, consider sharing it with a Python developer who's tired of fighting their framework on a small project. Sometimes the best tool is a well-chosen collection of small tools.
Top comments (0)