DEV Community

Kyle Rhodelander
Kyle Rhodelander

Posted on

Best Python Libraries for Building REST APIs Without a Framework in 2026

Best Python Libraries for Building REST APIs Without a Framework in 2026

Building REST APIs in Python doesn't always mean reaching for Django REST Framework or FastAPI. Sometimes you want more control, a leaner stack, or you're building something that doesn't need the full weight of a framework. Maybe you're embedding an API into an existing application, writing a microservice that needs to be razor-thin, or you simply want to understand what's happening under the hood.

In 2026, the Python ecosystem has matured enough that you can assemble a production-ready REST API from individual libraries — each doing one thing well — without the overhead or opinions of a full framework. This post walks you through the best libraries for doing exactly that.


Why Skip the Framework?

Frameworks are excellent until they get in your way. When you need fine-grained control over routing, serialization, middleware, or authentication — or when you're integrating Python API capabilities into a larger non-web application — rolling your own stack from focused libraries makes a lot of sense.

Benefits include:

  • Smaller footprint — fewer dependencies, faster cold starts (critical for serverless)
  • No framework lock-in — easier to evolve your architecture over time
  • Deeper understanding — you know exactly what each component does
  • Composability — swap out any piece independently

Let's break this down by the core concerns of any REST API.


HTTP Server and Request Handling

Starlette

Even without FastAPI wrapped around it, Starlette is one of the most capable ASGI-based HTTP toolkits available in Python. It handles routing, request/response objects, middleware, WebSocket support, and background tasks — all without imposing a full framework structure on you.

You can use Starlette's Router, Request, and Response classes directly:

from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route

async def list_users(request):
    return JSONResponse({"users": ["alice", "bob"]})

app = Starlette(routes=[
    Route("/users", list_users),
])
Enter fullscreen mode Exit fullscreen mode

Starlette gives you exactly what you ask for and nothing more. It's the foundation FastAPI is built on, which tells you everything about its quality.

Best for: Developers who want async-first HTTP handling with no framework dogma.


Werkzeug

If you're in the WSGI world, Werkzeug is the low-level toolkit Flask is built on. You get full control over request and response objects, URL routing via Map and Rule, and HTTP utilities without Flask's application conventions being imposed on you.

from werkzeug.wrappers import Request, Response
from werkzeug.routing import Map, Rule
from werkzeug.exceptions import NotFound

url_map = Map([
    Rule('/api/items', endpoint='list_items'),
])

def list_items(request):
    return Response('{"items": []}', content_type='application/json')
Enter fullscreen mode Exit fullscreen mode

Werkzeug is battle-tested, extremely well-documented, and has no runtime dependencies beyond itself. For synchronous APIs that need maximum stability, it remains a top-tier choice in 2026.

Best for: Synchronous APIs, teams already familiar with the Flask ecosystem, production-critical environments.


Uvicorn + raw ASGI

For the absolute minimal layer, you can write raw ASGI apps served by Uvicorn. This is mostly useful for educational purposes or hyper-optimized microservices, but it's worth knowing it's possible:

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'{"status": "ok"}'})
Enter fullscreen mode Exit fullscreen mode

You won't build something maintainable this way at scale, but understanding raw ASGI helps when debugging framework internals.


Routing

Routes (Standalone Mapper)

The Routes library provides RESTful route generation inspired by Ruby on Rails. It handles URL matching and generation independently of any web framework, making it easy to bolt on top of Werkzeug or any WSGI layer.

For simple projects, Starlette's built-in router handles most cases. For larger projects, you might consider a dedicated router like r3 which provides high-performance regex-based routing.


Data Validation and Serialization

Pydantic v2

No library has had a greater impact on Python API development in recent years than Pydantic. Version 2, rewritten in Rust, is dramatically faster than v1 and handles validation, serialization, and schema generation in one package.

from pydantic import BaseModel, EmailStr, field_validator

class UserCreate(BaseModel):
    name: str
    email: EmailStr
    age: int

    @field_validator('age')
    @classmethod
    def age_must_be_positive(cls, v):
        if v < 0:
            raise ValueError('Age must be positive')
        return v

# Validate incoming JSON
user = UserCreate.model_validate({"name": "Alice", "email": "alice@example.com", "age": 30})

# Serialize to dict or JSON
user.model_dump()
user.model_dump_json()
Enter fullscreen mode Exit fullscreen mode

Pydantic also generates JSON Schema automatically, which you can use to build OpenAPI docs without a framework.

Best for: Any API needing request validation and response serialization. This is non-negotiable in 2026.


Marshmallow

Marshmallow is the older but still widely used alternative to Pydantic. It's pure Python, has a large ecosystem of extensions, and some teams prefer its explicit, class-based approach. If you're working with ORMs like SQLAlchemy, marshmallow-sqlalchemy provides excellent integration.

Pydantic v2 is generally faster and more feature-rich today, but marshmallow's flexibility with custom field types and its ecosystem of plugins keeps it relevant.


Authentication and Authorization

PyJWT

Token-based authentication is the backbone of modern REST APIs. PyJWT is the standard library for encoding and decoding JSON Web Tokens in Python.

import jwt
from datetime import datetime, timedelta, timezone

SECRET = "your-secret-key"

def create_token(user_id: int) -> str:
    payload = {
        "sub": str(user_id),
        "exp": datetime.now(timezone.utc) + timedelta(hours=1)
    }
    return jwt.encode(payload, SECRET, algorithm="HS256")

def verify_token(token: str) -> dict:
    return jwt.decode(token, SECRET, algorithms=["HS256"])
Enter fullscreen mode Exit fullscreen mode

Pair this with a middleware layer in Starlette or a decorator in Werkzeug to protect your endpoints.

Best for: Any API requiring stateless authentication.


Authlib

For OAuth 2.0, OpenID Connect, or more sophisticated authentication flows, Authlib is the go-to library. It handles the full OAuth lifecycle including token introspection, JWT validation with JWKS, and integrates cleanly with both WSGI and ASGI applications.


Database Access

SQLAlchemy Core (Not the ORM)

While SQLAlchemy is famous for its ORM, SQLAlchemy Core is a powerful SQL abstraction layer that gives you full control over queries without the magic of object mapping. When building APIs without a framework, using Core often makes more sense than the ORM.

from sqlalchemy import create_engine, Table, MetaData, select

engine = create_engine("postgresql+psycopg://user:password@localhost/mydb")
metadata = MetaData()

users = Table('users', metadata, autoload_with=engine)

with engine.connect() as conn:
    result = conn.execute(select(users).where(users.c.active == True))
    rows = result.fetchall()
Enter fullscreen mode Exit fullscreen mode

Best for: Any project needing reliable, powerful database access without ORM overhead.


Databases (Async)

For async APIs built on Starlette or raw ASGI, databases provides an async interface to SQLAlchemy Core queries. It supports PostgreSQL (via asyncpg), MySQL, and SQLite, and plays well with connection pooling.


Middleware and Cross-Cutting Concerns

Python-Multipart

Handling file uploads and multipart form data requires python-multipart, a streaming multipart parser. If your API accepts file uploads, this is essential.

Limits

Limits is a rate limiting library that supports multiple storage backends (Redis, Memcached, in-memory). Without a framework providing this out of the box, you'll need something like Limits to prevent API abuse:

from limits import parse
from limits.storage import RedisStorage
from limits.strategies import FixedWindowRateLimiter

storage = RedisStorage("redis://localhost:6379")
limiter = FixedWindowRateLimiter(storage)
rate = parse("100/minute")

def check_rate_limit(user_id: str) -> bool:
    return limiter.hit(rate, "api", user_id)
Enter fullscreen mode Exit fullscreen mode

API Documentation

Spectree

Spectree is one of the cleanest ways to add OpenAPI documentation to a framework-free Python API. It integrates with Starlette, Falcon, and Flask, uses Pydantic models to generate schemas automatically, and serves a Swagger UI without you writing a single YAML file.

For teams wanting full control over their OpenAPI spec, apispec lets you build the specification programmatically and attach it to any documentation renderer.


Putting It All Together: A Minimal Example

Here's a sketch of what a no-framework REST API looks like combining Starlette, Pydantic, PyJWT, and SQLAlchemy:

from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware
from pydantic import BaseModel, ValidationError
import jwt

SECRET = "your-secret"

class AuthMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        token = request.headers.get("Authorization", "").replace("Bearer ", "")
        try:
            request.state.user = jwt.decode(token, SECRET, algorithms=["HS256"])
        except jwt.InvalidTokenError:
            return JSONResponse({"error": "Unauthorized"}, status_code=401)
        return await call_next(request)

class ItemCreate(BaseModel):
    name: str
    price: float

async def create_item(request):
    try:
        body = await request.json()
        item = ItemCreate.model_validate(body)
        # Save to database...
        return JSONResponse(item.model_dump(), status_code=201)
    except ValidationError as e:
        return JSONResponse({"errors": e.errors()}, status_code=422)

app = Starlette(
    routes=[Route("/items", create_item, methods=["POST"])],
    middleware=[Middleware(AuthMiddleware)]
)
Enter fullscreen mode Exit fullscreen mode

Clean, readable, and you know exactly what every line does.


Choosing the Right Combination

Concern Sync Stack Async Stack
HTTP Werkzeug Starlette
Validation Pydantic v2 Pydantic v2
Database SQLAlchemy Core databases + asyncpg
Auth PyJWT PyJWT + Authlib
Rate Limiting Limits + Redis Limits + Redis
Docs apispec Spectree

Final Thoughts

Building REST APIs without a framework in Python isn't a rejection of good tooling — it's a deliberate choice to use focused, composable libraries that each solve one problem well. The libraries covered here are all actively maintained, production-tested, and represent the best options available in 2026.

The key insight is that frameworks like FastAPI and Flask are themselves assembled from many of these same pieces. When you build without them, you gain the same capabilities with the added benefit of knowing exactly what's in your stack and why.

Start small. Pick an HTTP layer (Starlette for async, Werkzeug for sync), add Pydantic for validation, PyJWT for auth, and SQLAlchemy Core for database access. Add other libraries only when you genuinely need them. You'll end up with an API that's lean, fast, and entirely yours.


Ready to Start Building?

If this post helped clarify your options, bookmark it and share it with your team. Building Python APIs without a framework is a genuine skill — one that makes you a better developer even when you do reach for Django or FastAPI.

Try it yourself: Take one of the libraries above — start with Starlette and Pydantic v2 — and build a small CRUD API this weekend. You'll be surprised how little you actually need to ship something solid.

Have questions about a specific library or architecture pattern? Drop them in the comments below — I read every one.

Top comments (0)