Skip to main content

Express.js-like framework built on Django

Project description

shanks

Shanks Django

๐Ÿš€ Express.js-like framework built on Django. Write Django APIs with Express.js syntax and Prisma-like ORM.

PyPI version License: MIT

โœจ Features

  • ๐ŸŽฏ Express.js-like syntax - Familiar routing for Node.js developers
  • ๐Ÿ”ฅ Prisma-like ORM - Modern database queries with find_many(), create(), etc.
  • ๐Ÿš€ Built on Django - Full Django power under the hood
  • โšก Built-in Caching - Auto-cache GET requests, 10x faster responses
  • ๐Ÿ”ง Middleware support - Express-style middleware (req, res, next)
  • ๐Ÿ“ฆ Simple Request/Response - Clean API for handling HTTP
  • ๐ŸŽจ Built-in formatter - Black code formatting
  • ๐Ÿ” Built-in linter - Flake8 linting
  • โšก Auto-reload dev server - Like nodemon for Python
  • ๐Ÿ—„๏ธ Multi-database support - PostgreSQL, MySQL, MongoDB, Redis
  • ๐Ÿ“š Swagger/OpenAPI - Auto-generated API documentation
  • ๐ŸŒ CORS built-in - Easy cross-origin setup
  • ๐Ÿ—๏ธ Go-like Architecture - Clean project structure (internal/, entity/, dto/)
  • ๐ŸŽ VSCode Extension - Snippets and IntelliSense
  • ๐Ÿ› ๏ธ CLI Generator - Generate CRUD & Auth with one command

๐Ÿ“ฆ Installation

pip install shanks-django

With Database Support

# PostgreSQL
pip install shanks-django[postgres]

# MySQL
pip install shanks-django[mysql]

# MongoDB
pip install shanks-django[mongodb]

# Redis
pip install shanks-django[redis]

# All databases
pip install shanks-django[all]

๐Ÿš€ Quick Start

# Create new project with Go-like architecture
shanks new myproject
cd myproject

# Generate CRUD endpoints
shanks create posts --crud

# Generate auth endpoints  
shanks create auth --simple

# Run migrations
python manage.py makemigrations
python manage.py migrate

# Start server
shanks run

Visit:

That's it! You now have a fully functional API with:

  • โœ… Auto-caching (10x faster GET requests)
  • โœ… Smart cache invalidation
  • โœ… Swagger documentation
  • โœ… CRUD endpoints with pagination
  • โœ… Authentication endpoints
  • โœ… Go-like project structure

๐Ÿ’ก Simple Example

# internal/routes/__init__.py
from shanks import App, auto_cache, swagger

app = App()

# Built-in caching - enabled by default!
app.use(auto_cache)

# Auto-generated Swagger docs
app.use(swagger(title="My API"))

@app.get('api/posts')
def list_posts(req):
    # First request: fetches from DB, caches result
    # Next requests: served from cache (10x faster!)
    return {'posts': [...]}

@app.post('api/posts')
def create_post(req):
    # Automatically invalidates /api/posts cache
    post = Post.create(**req.body)
    return {'id': post.id}

urlpatterns = app.get_urls()

๐Ÿ› ๏ธ CLI Commands

# Development server with auto-reload (like nodemon)
shanks run                    # Start on 127.0.0.1:8000
shanks run 3000               # Start on port 3000
shanks run 0.0.0.0:8000       # Start on all interfaces

# Project management
shanks new myproject          # Create new project

# Generate CRUD endpoints (NEW!)
shanks create posts --crud    # Generate full CRUD with model
                              # Creates: model, routes with pagination & findById

# Generate Auth endpoints (NEW!)
shanks create auth --simple   # Generate /login, /register, /me
shanks create auth --complete # Generate /login, /register, /verify, /me

# Code quality
shanks format                 # Format with Black
shanks lint                   # Lint with Flake8
shanks check                  # Format + Lint

# Help
shanks help                   # Show all commands

Generate CRUD Endpoints

Quickly scaffold complete CRUD operations:

shanks create posts --crud

This creates:

  • app/models/posts.py - Model with SORM
  • app/routes/posts.py - Complete CRUD routes

Includes:

  • โœ… List with pagination (page, limit)
  • โœ… Get by ID (findById)
  • โœ… Create
  • โœ… Update
  • โœ… Delete
  • โœ… Auth checks
  • โœ… Error handling

Generate Auth Endpoints

Simple auth (login, register, me):

shanks create auth --simple

Complete auth (with email verification):

shanks create auth --complete

๐Ÿ“– Core Concepts

1. Routes (Express.js-like)

from shanks import App

app = App()

# GET route
@app.get('api/users')
def get_users(req):
    return {'users': []}

# POST route
@app.post('api/users')
def create_user(req):
    data = req.body
    return {'created': True, 'data': data}

# PUT route
@app.put('api/users/<int:user_id>')
def update_user(req, user_id):
    return {'updated': True, 'id': user_id}

# DELETE route
@app.delete('api/users/<int:user_id>')
def delete_user(req, user_id):
    return {'deleted': True, 'id': user_id}

# PATCH route
@app.patch('api/users/<int:user_id>')
def patch_user(req, user_id):
    return {'patched': True}

urlpatterns = app.get_urls()

2. Request Object

@app.post('api/data')
def handle_data(req):
    # Get JSON body
    data = req.body
    name = req.body.get('name')
    
    # Get query parameters
    page = req.query.get('page', 1)
    limit = req.query.get('limit', 10)
    
    # Get headers
    auth = req.headers.get('Authorization')
    content_type = req.headers.get('Content-Type')
    
    # Get cookies
    token = req.cookies.get('token')
    
    # Get uploaded files
    file = req.files.get('file')
    
    # Get authenticated user (Django)
    user = req.user
    is_authenticated = req.user.is_authenticated
    
    # Get session (Django)
    cart = req.session.get('cart', [])
    req.session['key'] = 'value'
    
    # HTTP method and path
    method = req.method  # GET, POST, etc.
    path = req.path      # /api/data
    
    # Access full Django request
    django_req = req.django
    
    return {'status': 'ok'}

3. Response Object

from shanks import App, Response

app = App()

# Simple JSON response
@app.get('api/data')
def get_data(req):
    return {'data': 'value'}  # Auto-converts to JSON

# Using Response object
@app.get('api/custom')
def custom(req):
    return Response().json({'data': 'value'})

# Custom status code
@app.post('api/create')
def create(req):
    return Response().status_code(201).json({'created': True})

# Set headers
@app.get('api/headers')
def with_headers(req):
    return (Response()
        .header('X-Custom-Header', 'value')
        .header('X-Another', 'value2')
        .json({'ok': True}))

# Set cookies
@app.post('api/login')
def login(req):
    return (Response()
        .cookie('token', 'abc123', max_age=3600)
        .cookie('refresh', 'xyz789', max_age=86400)
        .json({'logged_in': True}))

# Redirect
@app.get('old-url')
def old_url(req):
    return Response().redirect('/new-url')

# Render Django template
@app.get('dashboard')
def dashboard(req):
    context = {'title': 'Dashboard', 'user': req.user}
    return Response().render(req.django, 'dashboard.html', context)

# File download
@app.get('api/download')
def download(req):
    return Response().file('/path/to/file.pdf', 'document.pdf')

4. Middleware (Express.js-like)

from shanks import App, Response

app = App()

# Simple logging middleware - Express.js style!
def logger(req, res, next):
    print(f"{req.method} {req.path}")
    next()  # Continue to next middleware

app.use(logger)

# Auth middleware
def auth_middleware(req, res, next):
    token = req.headers.get('Authorization')
    if not token:
        return Response().status_code(401).json({'error': 'Unauthorized'})
    # If returns response, stops chain
    # Otherwise call next() to continue
    next()

app.use(auth_middleware)

# Modify response
def add_header(req, res, next):
    res.header('X-Custom-Header', 'value')
    next()

app.use(add_header)

# Multiple middleware
app.use(logger)
app.use(auth_middleware)
app.use(add_header)

@app.get('api/protected')
def protected(req):
    return {'data': 'secret'}

Middleware signature: def middleware(req, res, next)

  • req - Request object
  • res - Response object (can modify headers, etc)
  • next - Function to call next middleware

Return Response() to stop chain, or call next() to continue.

๐Ÿ—„๏ธ Prisma-like ORM

Shanks provides a modern, Prisma-inspired ORM syntax on top of Django ORM.

SORM - Super Simple JSON Syntax

Use SORM (Shanks ORM) with JSON-like syntax - JavaScript types!

from SORM import table

# Define model like JSON - JavaScript types!
Category = table("Category", {
    "name": "string:100:unique",      # string = CharField
    "slug": "slug:unique",
    "description": "text:blank",
    "views": "number",                 # number = IntegerField
    "active": "boolean",               # boolean = BooleanField
    "created_at": "date:auto_now_add"  # date = DateTimeField
})

# With relations
Post = table("Post", {
    "title": "string:200",
    "content": "text",
    "published": "boolean",
    "author": {"type": "relation", "model": "auth.User"},
    "tags": {"type": "many", "model": "app.Tag"},
    "created_at": "date:auto_now_add"
})

JavaScript-like types:

  • string - Text (CharField)
  • number - Integer (IntegerField)
  • boolean - True/False (BooleanField)
  • date - DateTime (DateTimeField)

Additional types:

  • text - Long text (TextField)
  • float - Decimal numbers
  • email, url, slug, json

Field syntax: "type:max_length:options"

  • "string:100" - CharField with max_length=100
  • "string:100:unique" - CharField with max_length=100, unique=True
  • "text:blank" - TextField with blank=True
  • "date:auto_now_add" - DateTimeField with auto_now_add=True
  • "number" - IntegerField
  • "boolean" - BooleanField

Relations:

  • {"type": "relation", "model": "app.Model"} - ForeignKey
  • {"type": "many", "model": "app.Model"} - ManyToManyField

That's it! No CharField, TextField, ForeignKey imports needed.

Define Models

from shanks import Model, CharField, TextField, DateTimeField, ForeignKey, CASCADE

class Post(Model):
    title = CharField(max_length=200)
    content = TextField()
    created_at = DateTimeField(auto_now_add=True)
    updated_at = DateTimeField(auto_now=True)
    
    class Meta:
        ordering = ['-created_at']

Available Field Types

from shanks import (
    CharField,          # String field
    TextField,          # Long text
    IntegerField,       # Integer
    FloatField,         # Float
    BooleanField,       # Boolean
    DateField,          # Date
    DateTimeField,      # DateTime
    EmailField,         # Email
    URLField,           # URL
    SlugField,          # Slug
    JSONField,          # JSON data
    
    # Relationships
    ForeignKey,         # One-to-many
    ManyToManyField,    # Many-to-many
    OneToOneField,      # One-to-one
    
    # Relationship options
    CASCADE,            # Delete related
    SET_NULL,           # Set to NULL
    PROTECT,            # Prevent deletion
)

Query Methods (Prisma-like)

from app.models import Post

# Find many records
posts = Post.find_many()
posts = Post.find_many(author=user)
posts = Post.find_many(title__contains='Django')

# Find first record
post = Post.find_first(slug='hello-world')
post = Post.find_first(author=user, published=True)

# Find unique record (returns None if not found)
post = Post.find_unique(id=1)
post = Post.find_unique(slug='hello-world')

# Create record
post = Post.create(
    title='Hello World',
    content='This is my first post',
    author=user
)

# Update records
Post.update(
    where={'author': user},
    data={'published': True}
)

# Delete many records
Post.delete_many(published=False)
Post.delete_many(created_at__lt=old_date)

# Count records
total = Post.count()
published_count = Post.count(published=True)

# Update instance
post = Post.find_unique(id=1)
post.update_self(title='New Title', content='New content')

# Delete instance
post = Post.find_unique(id=1)
post.delete_self()

User Model (Built-in)

from shanks import User, authenticate

# Find users
users = User.find_many()
user = User.find_unique(username='john')
user = User.find_first(email='john@example.com')

# Create user
user = User.create(
    username='john',
    email='john@example.com',
    password='secret123',  # Auto-hashed
    first_name='John',
    last_name='Doe'
)

# Authenticate user
user = authenticate(username='john', password='secret123')
if user:
    print('Login successful')

# Update user
user.update_self(
    first_name='Johnny',
    email='johnny@example.com'
)

# Update password
user.update_self(password='newpassword')  # Auto-hashed

# Count users
total_users = User.count()
active_users = User.count(is_active=True)

Complete CRUD Example

from shanks import App, Response, Model, CharField, TextField, ForeignKey, CASCADE, User
from shanks import slugify

# Define Model
class Post(Model):
    title = CharField(max_length=200)
    slug = CharField(max_length=200, unique=True)
    content = TextField()
    author = ForeignKey(User, on_delete=CASCADE, related_name='posts')
    
    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = slugify(self.title)
        super().save(*args, **kwargs)

# Routes
app = App()

# List all posts
@app.get('api/posts')
def list_posts(req):
    posts = Post.find_many()
    return {
        'posts': [{
            'id': p.id,
            'title': p.title,
            'slug': p.slug,
            'author': p.author.username
        } for p in posts]
    }

# Get single post
@app.get('api/posts/<int:post_id>')
def get_post(req, post_id):
    post = Post.find_unique(id=post_id)
    if not post:
        return Response().status_code(404).json({'error': 'Post not found'})
    
    return {
        'id': post.id,
        'title': post.title,
        'content': post.content,
        'author': post.author.username
    }

# Create post
@app.post('api/posts')
def create_post(req):
    post = Post.create(
        title=req.body.get('title'),
        content=req.body.get('content'),
        author=req.user
    )
    return Response().status_code(201).json({'id': post.id})

# Update post
@app.put('api/posts/<int:post_id>')
def update_post(req, post_id):
    post = Post.find_unique(id=post_id)
    if not post:
        return Response().status_code(404).json({'error': 'Not found'})
    
    post.update_self(
        title=req.body.get('title'),
        content=req.body.get('content')
    )
    return {'updated': True}

# Delete post
@app.delete('api/posts/<int:post_id>')
def delete_post(req, post_id):
    post = Post.find_unique(id=post_id)
    if not post:
        return Response().status_code(404).json({'error': 'Not found'})
    
    post.delete_self()
    return {'deleted': True}

urlpatterns = app.get_urls()

๐ŸŒ CORS Support

from shanks import App, CORS

app = App()

# Enable CORS for all origins (development)
CORS.enable(app)

# Production: Specific origins
CORS.enable(app,
    origins=['https://myapp.com', 'https://www.myapp.com'],
    methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
    headers=['Content-Type', 'Authorization', 'X-Custom-Header'],
    credentials=True,  # Allow cookies
    max_age=3600       # Cache preflight for 1 hour
)

# Multiple origins
CORS.enable(app,
    origins=[
        'http://localhost:3000',      # React dev
        'http://localhost:5173',      # Vite dev
        'https://myapp.com',          # Production
    ],
    credentials=True
)

@app.get('api/data')
def get_data(req):
    return {'data': 'value'}

๐Ÿ“š Swagger/OpenAPI Documentation

from shanks import App, swagger

app = App()

# Enable Swagger UI - Middleware style!
app.use(swagger(
    title="My API",
    version="1.0.0",
    description="Complete API documentation"
))

# All routes automatically documented!
@app.get('api/users')
def get_users(req):
    return {'users': []}

@app.post('api/users')
def create_user(req):
    data = req.body
    return {'created': True}

# Visit: http://localhost:8000/docs

That's it! All endpoints are automatically included in Swagger UI.

Legacy Style (Still Supported)

from shanks import App, SwaggerUI

app = App()

# Enable Swagger UI at /docs
SwaggerUI.enable(app,
    title="My API",
    version="1.0.0",
    description="Complete API documentation"
)

# Document routes with decorator (optional)
@app.get('api/users/<int:user_id>')
@SwaggerUI.doc(
    summary="Get user by ID",
    description="Returns a single user with all details",
    tags=["Users"],
    parameters=[{
        "name": "user_id",
        "in": "path",
        "required": True,
        "schema": {"type": "integer"},
        "description": "User ID"
    }],
    responses={
        200: {
            "description": "Success",
            "content": {
                "application/json": {
                    "schema": {
                        "type": "object",
                        "properties": {
                            "id": {"type": "integer"},
                            "username": {"type": "string"},
                            "email": {"type": "string"}
                        }
                    }
                }
            }
        },
        404: {"description": "User not found"}
    }
)
def get_user(req, user_id):
    user = User.find_unique(id=user_id)
    if not user:
        return Response().status_code(404).json({'error': 'Not found'})
    return {'id': user.id, 'username': user.username, 'email': user.email}

# Document POST with request body
@app.post('api/users')
@SwaggerUI.doc(
    summary="Create user",
    tags=["Users"],
    request_body={
        "required": True,
        "content": {
            "application/json": {
                "schema": {
                    "type": "object",
                    "required": ["username", "email", "password"],
                    "properties": {
                        "username": {"type": "string"},
                        "email": {"type": "string", "format": "email"},
                        "password": {"type": "string", "minLength": 8}
                    }
                }
            }
        }
    }
)
def create_user(req):
    user = User.create(
        username=req.body.get('username'),
        email=req.body.get('email'),
        password=req.body.get('password')
    )
    return Response().status_code(201).json({'id': user.id})

# Visit: http://localhost:8000/docs

๐Ÿ—„๏ธ Database Support

PostgreSQL

# settings.py
from shanks import DatabaseConfig

DATABASES = {
    'default': DatabaseConfig.postgres(
        host='localhost',
        port=5432,
        database='mydb',
        user='postgres',
        password='password'
    )
}

# Or from environment variable
import os
DATABASES = {
    'default': DatabaseConfig.from_url(os.getenv('DATABASE_URL'))
}
# DATABASE_URL format: postgresql://user:pass@host:port/dbname

MySQL

# settings.py
from shanks import DatabaseConfig

DATABASES = {
    'default': DatabaseConfig.mysql(
        host='localhost',
        port=3306,
        database='mydb',
        user='root',
        password='password'
    )
}

SQLite

# settings.py
from shanks import DatabaseConfig

DATABASES = {
    'default': DatabaseConfig.sqlite('db.sqlite3')
}

MongoDB

from shanks import MongoDB, App

# Connect to MongoDB
MongoDB.connect(
    host='localhost',
    port=27017,
    database='mydb',
    username='user',
    password='pass'
)

app = App()

# Use MongoDB
@app.get('api/products')
def get_products(req):
    products = list(MongoDB.db.products.find({}, {'_id': 0}))
    return {'products': products}

@app.post('api/products')
def create_product(req):
    product = {
        'name': req.body.get('name'),
        'price': req.body.get('price'),
        'stock': req.body.get('stock')
    }
    result = MongoDB.db.products.insert_one(product)
    return {'id': str(result.inserted_id)}

@app.get('api/products/<product_id>')
def get_product(req, product_id):
    from bson import ObjectId
    product = MongoDB.db.products.find_one({'_id': ObjectId(product_id)})
    if product:
        product['_id'] = str(product['_id'])
        return product
    return Response().status_code(404).json({'error': 'Not found'})

Redis

from shanks import Redis, App

# Connect to Redis
Redis.connect(
    host='localhost',
    port=6379,
    password='password',
    db=0
)

app = App()

# Cache example
@app.get('api/cache/<key>')
def get_cache(req, key):
    value = Redis.client.get(key)
    if value:
        return {'key': key, 'value': value.decode()}
    return Response().status_code(404).json({'error': 'Not found'})

@app.post('api/cache')
def set_cache(req):
    key = req.body.get('key')
    value = req.body.get('value')
    ttl = req.body.get('ttl', 3600)  # 1 hour default
    
    Redis.client.setex(key, ttl, value)
    return {'success': True, 'expires_in': ttl}

@app.delete('api/cache/<key>')
def delete_cache(req, key):
    Redis.client.delete(key)
    return {'deleted': True}

Multi-Database Example

from shanks import App, Model, CharField, ForeignKey, CASCADE, User
from shanks import MongoDB, Redis

# PostgreSQL Model (Django ORM)
class Product(Model):
    name = CharField(max_length=200)
    price = IntegerField()
    stock = IntegerField()

app = App()

@app.post('api/orders')
def create_order(req):
    # Get product from PostgreSQL
    product = Product.find_unique(id=req.body.get('product_id'))
    if not product:
        return Response().status_code(404).json({'error': 'Product not found'})
    
    # Check cache in Redis
    cache_key = f'stock:{product.id}'
    cached_stock = Redis.client.get(cache_key)
    
    # Create order in MongoDB
    order = {
        'user_id': req.user.id,
        'product_id': product.id,
        'product_name': product.name,
        'price': product.price,
        'quantity': req.body.get('quantity'),
        'status': 'pending'
    }
    result = MongoDB.db.orders.insert_one(order)
    
    # Update stock in PostgreSQL
    product.update_self(stock=product.stock - order['quantity'])
    
    # Update cache in Redis
    Redis.client.setex(cache_key, 3600, product.stock)
    
    return {'order_id': str(result.inserted_id)}

๐Ÿ” Authentication & Authorization

JWT Authentication Example

from shanks import App, Response, User, authenticate
import jwt
from datetime import datetime, timedelta

app = App()

SECRET_KEY = 'your-secret-key'

def create_token(user_id, username):
    payload = {
        'user_id': user_id,
        'username': username,
        'exp': datetime.utcnow() + timedelta(days=7)
    }
    return jwt.encode(payload, SECRET_KEY, algorithm='HS256')

def verify_token(token):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
        return payload
    except jwt.ExpiredSignatureError:
        return None
    except jwt.InvalidTokenError:
        return None

# Auth middleware
def auth_required(req):
    auth_header = req.headers.get('Authorization')
    if not auth_header or not auth_header.startswith('Bearer '):
        return Response().status_code(401).json({'error': 'Unauthorized'})
    
    token = auth_header.split(' ')[1]
    payload = verify_token(token)
    if not payload:
        return Response().status_code(401).json({'error': 'Invalid token'})
    
    # Attach user to request
    req.user = User.find_unique(id=payload['user_id'])
    if not req.user:
        return Response().status_code(401).json({'error': 'User not found'})

# Register
@app.post('api/auth/register')
def register(req):
    username = req.body.get('username')
    email = req.body.get('email')
    password = req.body.get('password')
    
    if User.find_unique(username=username):
        return Response().status_code(400).json({'error': 'Username exists'})
    
    user = User.create(username=username, email=email, password=password)
    token = create_token(user.id, user.username)
    
    return Response().status_code(201).json({
        'token': token,
        'user': {'id': user.id, 'username': user.username, 'email': user.email}
    })

# Login
@app.post('api/auth/login')
def login(req):
    username = req.body.get('username')
    password = req.body.get('password')
    
    user = authenticate(username=username, password=password)
    if not user:
        return Response().status_code(401).json({'error': 'Invalid credentials'})
    
    token = create_token(user.id, user.username)
    return {'token': token, 'user': {'id': user.id, 'username': user.username}}

# Protected route
@app.get('api/auth/me')
def get_me(req):
    auth_response = auth_required(req)
    if auth_response:
        return auth_response
    
    return {
        'id': req.user.id,
        'username': req.user.username,
        'email': req.user.email
    }

๐ŸŽฏ Advanced Features

URL Parameters

# Path parameters
@app.get('api/users/<int:user_id>')
def get_user(req, user_id):
    return {'id': user_id}

@app.get('api/posts/<slug:slug>')
def get_post(req, slug):
    return {'slug': slug}

@app.get('api/files/<path:filepath>')
def get_file(req, filepath):
    return {'path': filepath}

# Multiple parameters
@app.get('api/users/<int:user_id>/posts/<int:post_id>')
def get_user_post(req, user_id, post_id):
    return {'user_id': user_id, 'post_id': post_id}

# Query parameters
@app.get('api/search')
def search(req):
    query = req.query.get('q')
    page = int(req.query.get('page', 1))
    limit = int(req.query.get('limit', 10))
    
    # /api/search?q=django&page=2&limit=20
    return {'query': query, 'page': page, 'limit': limit}

File Uploads

@app.post('api/upload')
def upload_file(req):
    file = req.files.get('file')
    if not file:
        return Response().status_code(400).json({'error': 'No file provided'})
    
    # Save file
    import os
    upload_dir = 'uploads'
    os.makedirs(upload_dir, exist_ok=True)
    
    filepath = os.path.join(upload_dir, file.name)
    with open(filepath, 'wb') as f:
        for chunk in file.chunks():
            f.write(chunk)
    
    return {
        'filename': file.name,
        'size': file.size,
        'content_type': file.content_type,
        'path': filepath
    }

# Multiple files
@app.post('api/upload-multiple')
def upload_multiple(req):
    files = req.files.getlist('files')
    uploaded = []
    
    for file in files:
        # Save each file
        filepath = f'uploads/{file.name}'
        with open(filepath, 'wb') as f:
            for chunk in file.chunks():
                f.write(chunk)
        uploaded.append({'name': file.name, 'size': file.size})
    
    return {'uploaded': uploaded, 'count': len(uploaded)}

Pagination Helper

from shanks import App, Response

def paginate(queryset, page, limit):
    total = queryset.count()
    start = (page - 1) * limit
    end = start + limit
    items = queryset[start:end]
    
    return {
        'items': items,
        'total': total,
        'page': page,
        'limit': limit,
        'pages': (total + limit - 1) // limit
    }

@app.get('api/posts')
def list_posts(req):
    page = int(req.query.get('page', 1))
    limit = int(req.query.get('limit', 10))
    
    posts = Post.find_many()
    result = paginate(posts, page, limit)
    
    return {
        'posts': [{'id': p.id, 'title': p.title} for p in result['items']],
        'pagination': {
            'total': result['total'],
            'page': result['page'],
            'limit': result['limit'],
            'pages': result['pages']
        }
    }

Error Handling

from shanks import App, Response

app = App()

# Global error handler middleware
def error_handler(req):
    try:
        # Continue to next middleware/route
        return None
    except ValueError as e:
        return Response().status_code(400).json({'error': str(e)})
    except PermissionError:
        return Response().status_code(403).json({'error': 'Forbidden'})
    except Exception as e:
        return Response().status_code(500).json({'error': 'Internal server error'})

app.use(error_handler)

# Route-specific error handling
@app.get('api/data/<int:id>')
def get_data(req, id):
    try:
        data = Data.find_unique(id=id)
        if not data:
            return Response().status_code(404).json({'error': 'Not found'})
        return {'data': data}
    except ValueError:
        return Response().status_code(400).json({'error': 'Invalid ID'})
    except Exception as e:
        return Response().status_code(500).json({'error': str(e)})

๐Ÿš€ Full Stack Example (React + Shanks)

Backend (Shanks)

from shanks import App, CORS, SwaggerUI, Model, CharField, TextField, ForeignKey, CASCADE, User
from shanks import authenticate, Response

# Models
class Post(Model):
    title = CharField(max_length=200)
    content = TextField()
    author = ForeignKey(User, on_delete=CASCADE, related_name='posts')

# App
app = App()

# Enable CORS for React
CORS.enable(app,
    origins=['http://localhost:3000'],
    credentials=True
)

# Enable Swagger
SwaggerUI.enable(app, title='Blog API', version='1.0.0')

# Auth
@app.post('api/auth/login')
def login(req):
    user = authenticate(
        username=req.body.get('username'),
        password=req.body.get('password')
    )
    if not user:
        return Response().status_code(401).json({'error': 'Invalid credentials'})
    
    # Create session or JWT token here
    return {'user': {'id': user.id, 'username': user.username}}

# Posts
@app.get('api/posts')
def list_posts(req):
    posts = Post.find_many()
    return {
        'posts': [{
            'id': p.id,
            'title': p.title,
            'content': p.content,
            'author': p.author.username
        } for p in posts]
    }

@app.post('api/posts')
def create_post(req):
    if not req.user.is_authenticated:
        return Response().status_code(401).json({'error': 'Unauthorized'})
    
    post = Post.create(
        title=req.body.get('title'),
        content=req.body.get('content'),
        author=req.user
    )
    return Response().status_code(201).json({'id': post.id})

urlpatterns = app.get_urls()

Frontend (React)

// api.js
const API_URL = 'http://localhost:8000/api';

export async function login(username, password) {
  const response = await fetch(`${API_URL}/auth/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    credentials: 'include',
    body: JSON.stringify({ username, password })
  });
  return response.json();
}

export async function getPosts() {
  const response = await fetch(`${API_URL}/posts`, {
    credentials: 'include'
  });
  return response.json();
}

export async function createPost(title, content) {
  const response = await fetch(`${API_URL}/posts`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    credentials: 'include',
    body: JSON.stringify({ title, content })
  });
  return response.json();
}

// App.jsx
import { useState, useEffect } from 'react';
import { getPosts, createPost } from './api';

function App() {
  const [posts, setPosts] = useState([]);
  
  useEffect(() => {
    getPosts().then(data => setPosts(data.posts));
  }, []);
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    const formData = new FormData(e.target);
    await createPost(formData.get('title'), formData.get('content'));
    const data = await getPosts();
    setPosts(data.posts);
  };
  
  return (
    <div>
      <h1>Blog Posts</h1>
      <form onSubmit={handleSubmit}>
        <input name="title" placeholder="Title" required />
        <textarea name="content" placeholder="Content" required />
        <button type="submit">Create Post</button>
      </form>
      <ul>
        {posts.map(post => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.content}</p>
            <small>by {post.author}</small>
          </li>
        ))}
      </ul>
    </div>
  );
}

๐ŸŽจ VSCode Extension

Install the Shanks Django extension for VSCode to boost productivity!

Installation

  1. Open VSCode
  2. Go to Extensions (Ctrl+Shift+X)
  3. Search for "Shanks Django"
  4. Click Install

Or install from: https://marketplace.visualstudio.com/items?itemName=Ararya.shanks-django

Available Snippets

Prefix Description
shanks-app Create new Shanks app
shanks-get GET route
shanks-post POST route
shanks-put PUT route
shanks-delete DELETE route
shanks-middleware Middleware function
shanks-auth Auth middleware
shanks-cors Enable CORS
shanks-swagger Enable Swagger
shanks-doc Swagger documentation
shanks-response Response object
shanks-cookie Response with cookie
shanks-redirect Redirect response
shanks-model Create model
shanks-find-many Find many records
shanks-find-first Find first record
shanks-find-unique Find unique record
shanks-create Create record
shanks-update Update records
shanks-delete-many Delete records
shanks-count Count records
shanks-update-self Update instance
shanks-delete-self Delete instance
shanks-user-create Create user
shanks-authenticate Authenticate user
shanks-mongodb MongoDB setup
shanks-redis Redis setup
shanks-postgres PostgreSQL setup
shanks-full Full API template

Usage

Type the snippet prefix and press Tab to expand!

Example:

# Type: shanks-get [Tab]
@app.get('api/endpoint')
def handler(req):
    return {'data': 'value'}

๐Ÿ“ Project Structure

Recommended Structure

myproject/
โ”œโ”€โ”€ manage.py
โ”œโ”€โ”€ requirements.txt
โ”œโ”€โ”€ .env
โ”œโ”€โ”€ myproject/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ settings.py
โ”‚   โ”œโ”€โ”€ urls.py
โ”‚   โ””โ”€โ”€ wsgi.py
โ”œโ”€โ”€ app/
โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”œโ”€โ”€ models/
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ user.py
โ”‚   โ”‚   โ”œโ”€โ”€ post.py
โ”‚   โ”‚   โ””โ”€โ”€ comment.py
โ”‚   โ”œโ”€โ”€ routes/
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ auth.py
โ”‚   โ”‚   โ”œโ”€โ”€ posts.py
โ”‚   โ”‚   โ””โ”€โ”€ comments.py
โ”‚   โ”œโ”€โ”€ middleware/
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ auth.py
โ”‚   โ”‚   โ””โ”€โ”€ logger.py
โ”‚   โ”œโ”€โ”€ dto/
โ”‚   โ”‚   โ”œโ”€โ”€ __init__.py
โ”‚   โ”‚   โ”œโ”€โ”€ user.py
โ”‚   โ”‚   โ””โ”€โ”€ post.py
โ”‚   โ””โ”€โ”€ utils/
โ”‚       โ”œโ”€โ”€ __init__.py
โ”‚       โ”œโ”€โ”€ jwt.py
โ”‚       โ””โ”€โ”€ validators.py
โ”œโ”€โ”€ templates/
โ”‚   โ””โ”€โ”€ index.html
โ””โ”€โ”€ static/
    โ”œโ”€โ”€ css/
    โ””โ”€โ”€ js/

Example: Clean Architecture

# app/models/post.py
from shanks import Model, CharField, TextField, ForeignKey, CASCADE, User

class Post(Model):
    title = CharField(max_length=200)
    content = TextField()
    author = ForeignKey(User, on_delete=CASCADE)

# app/dto/post.py
class PostDTO:
    def __init__(self, data):
        self.title = data.get('title')
        self.content = data.get('content')
    
    def validate(self):
        errors = []
        if not self.title:
            errors.append('Title is required')
        if not self.content:
            errors.append('Content is required')
        return errors

# app/middleware/auth.py
from shanks import Response

def auth_required(req):
    if not req.user.is_authenticated:
        return Response().status_code(401).json({'error': 'Unauthorized'})

# app/routes/posts.py
from shanks import App, Response
from app.models import Post
from app.dto import PostDTO
from app.middleware import auth_required

router = App()

@router.get('api/posts')
def list_posts(req):
    posts = Post.find_many()
    return {'posts': [{'id': p.id, 'title': p.title} for p in posts]}

@router.post('api/posts')
def create_post(req):
    auth_response = auth_required(req)
    if auth_response:
        return auth_response
    
    dto = PostDTO(req.body)
    errors = dto.validate()
    if errors:
        return Response().status_code(400).json({'errors': errors})
    
    post = Post.create(title=dto.title, content=dto.content, author=req.user)
    return Response().status_code(201).json({'id': post.id})

# myproject/urls.py
from django.urls import path, include
from app.routes import posts

urlpatterns = [
    path('', include(posts.router.get_urls())),
]

๐Ÿ”ง Utility Functions

Shanks wraps common Django utilities for convenience:

from shanks import slugify

# Convert text to URL-friendly slug
slug = slugify('Hello World!')  # 'hello-world'
slug = slugify('Cafรฉ & Restaurant')  # 'cafe-restaurant'
slug = slugify('Python 3.11 Release')  # 'python-311-release'

# Unicode support
slug = slugify('ใ“ใ‚“ใซใกใฏ', allow_unicode=True)  # 'ใ“ใ‚“ใซใกใฏ'

๐Ÿงช Testing

# tests/test_api.py
from django.test import TestCase, Client
from shanks import User
from app.models import Post

class APITestCase(TestCase):
    def setUp(self):
        self.client = Client()
        self.user = User.create(
            username='testuser',
            email='test@example.com',
            password='testpass123'
        )
    
    def test_list_posts(self):
        # Create test data
        Post.create(title='Test Post', content='Content', author=self.user)
        
        # Test API
        response = self.client.get('/api/posts')
        self.assertEqual(response.status_code, 200)
        data = response.json()
        self.assertEqual(len(data['posts']), 1)
        self.assertEqual(data['posts'][0]['title'], 'Test Post')
    
    def test_create_post(self):
        # Login
        self.client.login(username='testuser', password='testpass123')
        
        # Create post
        response = self.client.post('/api/posts', {
            'title': 'New Post',
            'content': 'New Content'
        }, content_type='application/json')
        
        self.assertEqual(response.status_code, 201)
        self.assertTrue(Post.find_unique(title='New Post'))

# Run tests
# python manage.py test
# or
# shanks test

๐Ÿš€ Deployment

Production Settings

# settings.py
import os
from shanks import DatabaseConfig

DEBUG = False
ALLOWED_HOSTS = ['yourdomain.com', 'www.yourdomain.com']

# Database
DATABASES = {
    'default': DatabaseConfig.from_url(os.getenv('DATABASE_URL'))
}

# Security
SECRET_KEY = os.getenv('SECRET_KEY')
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

# Static files
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATIC_URL = '/static/'

Docker

# Dockerfile
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

RUN python manage.py collectstatic --noinput
RUN python manage.py migrate

EXPOSE 8000

CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"]
# docker-compose.yml
version: '3.8'

services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
      - SECRET_KEY=your-secret-key
    depends_on:
      - db
  
  db:
    image: postgres:15
    environment:
      - POSTGRES_DB=mydb
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

Heroku

# Install Heroku CLI
# heroku login

# Create app
heroku create myapp

# Add PostgreSQL
heroku addons:create heroku-postgresql:mini

# Set environment variables
heroku config:set SECRET_KEY=your-secret-key

# Deploy
git push heroku main

# Run migrations
heroku run python manage.py migrate

Railway

# Install Railway CLI
# railway login

# Initialize project
railway init

# Add PostgreSQL
railway add

# Deploy
railway up

๐Ÿ“š API Reference

App Class

from shanks import App

app = App()

# HTTP Methods
app.get(path)       # GET route
app.post(path)      # POST route
app.put(path)       # PUT route
app.delete(path)    # DELETE route
app.patch(path)     # PATCH route

# Middleware
app.use(middleware_function)

# Get Django URL patterns
app.get_urls()

Request Object

req.body            # JSON or form data (dict)
req.query           # Query parameters (dict)
req.headers         # HTTP headers (dict)
req.cookies         # Cookies (dict)
req.files           # Uploaded files (dict)
req.user            # Django user object
req.session         # Django session
req.method          # HTTP method (GET, POST, etc.)
req.path            # Request path
req.django          # Original Django request

Response Object

from shanks import Response

Response()
  .json(data)                    # JSON response
  .status_code(code)             # Set status code
  .header(key, value)            # Set header
  .cookie(key, value, **options) # Set cookie
  .redirect(url)                 # Redirect
  .render(request, template, context)  # Render template
  .file(filepath, filename)      # File download

Model Class (Prisma-like ORM)

from shanks import Model

# Query methods
Model.find_many(**filters)      # Find multiple records
Model.find_first(**filters)     # Find first record
Model.find_unique(**filters)    # Find unique record (returns None if not found)
Model.create(**data)            # Create record
Model.update(where, data)       # Update records
Model.delete_many(**filters)    # Delete records
Model.count(**filters)          # Count records

# Instance methods
instance.update_self(**data)    # Update instance
instance.delete_self()          # Delete instance

User Model

from shanks import User, authenticate

# Query methods
User.find_many(**filters)
User.find_first(**filters)
User.find_unique(**filters)
User.create(username, email, password, **kwargs)
User.count(**filters)

# Authentication
authenticate(username, password)  # Returns User or None

Database Helpers

from shanks import DatabaseConfig, MongoDB, Redis

# PostgreSQL/MySQL/SQLite
DatabaseConfig.postgres(host, database, user, password, port=5432)
DatabaseConfig.mysql(host, database, user, password, port=3306)
DatabaseConfig.sqlite(path)
DatabaseConfig.from_url(url)

# MongoDB
MongoDB.connect(host, database, username, password, port=27017)
MongoDB.db  # Access database

# Redis
Redis.connect(host, password, port=6379, db=0)
Redis.client  # Access client

CORS

from shanks import CORS

CORS.enable(app,
    origins=['*'],              # Allowed origins
    methods=['GET', 'POST'],    # Allowed methods
    headers=['Content-Type'],   # Allowed headers
    credentials=False,          # Allow credentials
    max_age=3600               # Preflight cache time
)

Swagger/OpenAPI

from shanks import SwaggerUI

SwaggerUI.enable(app,
    title='API Title',
    version='1.0.0',
    description='API Description'
)

@SwaggerUI.doc(
    summary='Endpoint summary',
    description='Detailed description',
    tags=['Tag'],
    parameters=[...],
    request_body={...},
    responses={...}
)

Built-in Caching

Shanks includes automatic caching for GET requests - 10x faster responses with zero configuration!

from shanks import App, auto_cache, smart_cache_invalidation

app = App()

# Enable auto-caching (enabled by default in new projects)
app.use(auto_cache)  # Auto-cache all GET requests for 5 minutes

# Smart cache invalidation (enabled by default)
app.use(smart_cache_invalidation)  # Auto-clear cache on POST/PUT/DELETE

How it works:

  1. First GET request โ†’ Fetches from database, caches result
  2. Subsequent GET requests โ†’ Served from cache (10x faster!)
  3. POST/PUT/DELETE โ†’ Automatically invalidates related cache
  4. Next GET request โ†’ Fresh data fetched and cached

Custom cache TTL:

from shanks import cache

@app.get("api/posts")
@cache(ttl=600)  # Cache for 10 minutes
def list_posts(req):
    return {"posts": [...]}

@app.get("api/stats")
@cache(ttl=3600)  # Cache for 1 hour
def get_stats(req):
    return {"stats": {...}}

Manual cache control:

from shanks import invalidate_cache, get_cache

# Clear all cache
invalidate_cache()

# Clear specific pattern
invalidate_cache("/api/posts")

# Direct cache access
cache = get_cache()
cache.set("key", "value", ttl=300)
value = cache.get("key")
cache.delete("key")

Benefits:

  • โšก 10x faster response times
  • ๐Ÿ”„ Automatic - no code changes needed
  • ๐Ÿง  Smart invalidation on writes
  • ๐Ÿ’พ Memory efficient with TTL
  • ๐ŸŽฏ Pattern-based invalidation

Utilities

from shanks import slugify

slugify(text, allow_unicode=False)  # Convert to URL-friendly slug

๐Ÿค Contributing

Contributions are welcome! Please check out the Contributing Guide.

Development Setup

# Clone repository
git clone https://github.com/Ararya/shanks-django.git
cd shanks-django

# Create virtual environment
python -m venv .venv
source .venv/bin/activate  # On Windows: .venv\Scripts\activate

# Install dependencies
pip install -e ".[dev]"

# Run tests
pytest

# Format code
black .
isort .

# Lint
flake8

๐Ÿ“ License

MIT License - see LICENSE file for details.

๐Ÿ”— Links

๐Ÿ’ฌ Community

โญ Show Your Support

If you like Shanks Django, please give it a star on GitHub! โญ

๐Ÿ“Š Comparison

Feature Django Express.js Shanks Django
Syntax Django Express.js Express.js
ORM Django ORM None Prisma-like
Routing URL patterns Decorators Decorators
Middleware Django middleware Functions Functions
Auto-reload โœ… โœ… (nodemon) โœ…
CORS django-cors-headers cors Built-in
Swagger drf-spectacular swagger-ui Built-in
Database PostgreSQL, MySQL, SQLite Any All + MongoDB, Redis
Learning Curve Steep Easy Easy

๐ŸŽ“ Learn More

๐Ÿ™ Acknowledgments


Made with โค๏ธ by Ararya

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

shanks_django-0.1.0.tar.gz (65.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

shanks_django-0.1.0-py3-none-any.whl (42.4 kB view details)

Uploaded Python 3

File details

Details for the file shanks_django-0.1.0.tar.gz.

File metadata

  • Download URL: shanks_django-0.1.0.tar.gz
  • Upload date:
  • Size: 65.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for shanks_django-0.1.0.tar.gz
Algorithm Hash digest
SHA256 1676483ddbadf6433f68c2d19333c8cdc47a88cea74c2100313f1c5b8ce8bfb0
MD5 4fc78dd14c3a40074501a418c5d2a28c
BLAKE2b-256 44b5bcc7e6af21291edf7cc1e7b883c2744c9894754601b9d63d701693b53905

See more details on using hashes here.

File details

Details for the file shanks_django-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: shanks_django-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 42.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for shanks_django-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2c8fecebdd6686dd918ddb3be69de9ac7cf51a793c9603d8d4df7db8b2ee9eef
MD5 98fd92a8f3b51abb60af4842ef05cd64
BLAKE2b-256 8a4efb336f48fda4619197fe51878283e57c84a4728e79e76f6a92f31e60725f

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page