A Laravel-flavored Django starter with Inertia-style pages and artisan-like commands.
Project description
Larajango
Larajango is a Django starter shaped like Laravel 13: controllers in app/Http/Controllers, requests in app/Http/Requests, middleware in app/Http/Middleware, models in app/Models, route files in routes, config in config, an artisan command line, and Inertia-style pages in resources/js/Pages.
Frontend assets are served with Vite through django-vite. In development, Django renders the Vite HMR client, React refresh preamble, and resources/js/app.jsx from the Vite dev server. In production, run npm run build so Django can read public/build/manifest.json and serve the compiled files from /static/build/.
Quick Start
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
npm install
./artisan migrate
./artisan dev
Or run the servers separately:
./artisan serve
npm run dev
Open http://127.0.0.1:8000.
The Python requirements include django-vite and inertia-django; install them with pip install -r requirements.txt.
Commands
./artisan serve
./artisan dev
./artisan migrate
./artisan route:list -v
./artisan route:list -vv
./artisan route:list --path=api
./artisan route:cache
./artisan route:clear
./artisan config:show app
./artisan config:clear
./artisan cache:clear
./artisan db:seed
./artisan key:generate
./artisan storage:link
./artisan make:controller PostController
./artisan make:controller PostController --resource
./artisan make:model Post -m
./artisan make:middleware EnsureUserIsAdmin
./artisan make:middleware EnsureUserHasRole --parameters role
./artisan make:middleware LogAfterResponse --terminable
./artisan make:request StorePostRequest
./artisan make:seeder UserSeeder
./artisan make:policy PostPolicy
./artisan make:job SendWelcomeEmail
./artisan make:provider AppServiceProvider
./artisan make:migration create_posts_table
./artisan inertia:page Posts/Index
./artisan install:api
You can also create another project from this framework package:
python -m larajango new blog
Routing
Larajango tracks Laravel 13's routing API shape as closely as Django reasonably allows. Add routes in routes/web.py:
from app.Http.Controllers.HomeController import HomeController
from larajango.routing import router
router.get("/", HomeController.index, name="home")
router.post("/profile", ProfileController.update)
router.put("/posts/{post}", PostController.update)
router.patch("/posts/{post}", PostController.update)
router.delete("/posts/{post}", PostController.destroy)
router.options("/ping", lambda request: response(status=204))
router.match(["GET", "POST"], "/submit", SubmitController.handle)
router.any("/webhook", WebhookController.handle)
Redirect and view routes are available:
router.redirect("/home", "/", status=302)
router.permanent_redirect("/old-home", "/")
router.view("/welcome", "welcome.html", {"name": "Taylor"})
Route parameters support required, optional, constrained, and catch-all segments:
router.get("/users/{id}", UserController.show).where_number("id")
router.get("/users/{name?}", UserController.show).where_alpha("name")
router.get("/category/{category}", CategoryController.show).where_in("category", ["movie", "song"])
router.get("/files/{path}", FileController.show).where("path", ".*")
You can define global constraints in app/Providers/AppServiceProvider.py:
from larajango.support import Route
class AppServiceProvider(ServiceProvider):
def boot(self):
Route.pattern("id", "[0-9]+")
Named routes are generated with larajango.urls.route:
from larajango.urls import route
profile = route("profile")
Groups support middleware, prefixes, names, domains, controller shorthand, and scoped binding flags:
with router.group(prefix="admin", name="admin.", middleware=["auth"]):
router.get("/dashboard", HomeController.index, name="dashboard")
router.middleware(["auth", "throttle:api"]).prefix("account").name("account.").group(
lambda: router.get("/", AccountController.index, name="index")
)
router.domain("{account}.example.com").group(
lambda: router.get("/dashboard", TenantController.dashboard, name="tenant.dashboard")
)
router.controller(PostController).prefix("posts").name("posts.").group(lambda: (
router.get("/", "index", name="index"),
router.post("/", "store", name="store"),
))
Controller actions can return normal Django responses or Inertia pages:
from larajango.inertia import inertia
class HomeController:
def index(request):
return inertia(request, "Home", {"framework": "Larajango"})
Resource controllers are supported:
from app.Http.Controllers.PostController import PostController
from larajango.routing import router
router.resource("posts", PostController)
router.api_resource("api/posts", PostController)
API routes live in routes/api.py and are automatically prefixed with /api.
Route model binding can be explicit:
from app.Models.Post import Post
from larajango.support import Route
class AppServiceProvider(ServiceProvider):
def boot(self):
Route.model("post", Post)
Route.bind("slug", lambda value: Post.objects.get(slug=value))
Route parameters can also use Python enum annotations in controller functions. Invalid enum values return 404.
Fallback routes, current-route access, route caching, and method spoofing are included:
router.fallback(lambda request, path=None: response("Not found", status=404))
current = router.current(request)
name = router.current_route_name(request)
action = router.current_route_action(request)
{% load forms %}
<form method="POST" action="/posts/1">
{% csrf %}
{% method "PUT" %}
</form>
./artisan route:list -vv
./artisan route:cache
./artisan route:clear
Middleware
Register middleware aliases and groups in bootstrap/app.py:
from larajango.foundation import Middleware
from larajango.routing import router
middleware = Middleware(router)
middleware.alias({
"auth": "app.Http.Middleware.Authenticate.Authenticate",
"role": "app.Http.Middleware.EnsureUserHasRole.EnsureUserHasRole",
})
middleware.group("api", ("throttle:api",))
Then attach them to route groups:
with router.group(prefix="account", middleware=["auth"]):
router.get("/", AccountController.index, name="account")
Middleware may be attached to individual routes, excluded from routes, or chained through route groups:
router.get("/profile", ProfileController.show).middleware("auth")
router.get("/public", PublicController.index).without_middleware("auth")
router.middleware(["auth", "role:admin"]).prefix("admin").group(
lambda: router.get("/", AdminController.index, name="admin")
)
Middleware parameters are passed after the middleware name:
class EnsureUserHasRole:
def __init__(self, next_handler, role):
self.next_handler = next_handler
self.role = role
def __call__(self, request, *args, **kwargs):
if not request.user.groups.filter(name=self.role).exists():
return response("Forbidden", status=403)
return self.next_handler(request, *args, **kwargs)
Middleware groups can be modified Laravel-style:
middleware.api(append=["throttle:api"])
middleware.web(prepend=["app.Http.Middleware.BeforeRequest.BeforeRequest"])
middleware.web(replace={"old": "new"})
middleware.web(remove=["auth"])
middleware.priority(["auth", "throttle:api"])
Terminable middleware can define terminate(request, response):
class LogAfterResponse:
def __init__(self, next_handler):
self.next_handler = next_handler
def __call__(self, request, *args, **kwargs):
return self.next_handler(request, *args, **kwargs)
def terminate(self, request, response):
pass
Rate limiters follow Laravel's Limit builder style:
from larajango.rate_limiting import Limit, RateLimiter
RateLimiter.for_("uploads", lambda request: Limit.per_minute(100).by(request.META["REMOTE_ADDR"]))
with router.group(middleware=["throttle:uploads"]):
router.post("/upload", UploadController.store)
CORS OPTIONS responses are handled by larajango.middleware.CorsMiddleware; configure defaults in config/cors.py or .env.
CSRF Protection
Larajango uses Django's CSRF engine with Laravel-style configuration and SPA conveniences, following Laravel 13's CSRF model.
Configure request forgery protection in bootstrap/app.py:
middleware.preventRequestForgery(
except_paths=["stripe/*", "http://example.com/foo/*"],
origin_only=False,
allow_same_site=False,
xsrf_cookie=True,
)
The same values may be set in .env through config/csrf.py:
CSRF_ORIGIN_ONLY=false
CSRF_ALLOW_SAME_SITE=false
CSRF_XSRF_COOKIE=true
CSRF_EXCEPT=stripe/*,webhook/*
Include a token in HTML forms:
{% load forms %}
<form method="POST" action="/profile">
{% csrf %}
<button type="submit">Save</button>
</form>
Expose the token to JavaScript with a meta tag:
{% load forms %}
{% csrf_meta %}
AJAX requests may send either header:
X-CSRF-TOKEN: <token>
X-XSRF-TOKEN: <token>
When enabled, Larajango also sends an XSRF-TOKEN cookie for Axios/Angular-style same-origin clients. Modern browser Sec-Fetch-Site: same-origin requests are accepted before token fallback; allow_same_site=True also accepts same-site.
Requests
Form requests provide Laravel-style validation:
from app.Http.Requests.StorePostRequest import StorePostRequest
from larajango.requests import validate
class PostController:
@validate(StorePostRequest)
def store(request):
data = request.validated
Configuration
Copy .env.example to .env and edit values such as APP_NAME, APP_DEBUG, and DB_DATABASE. Read values from Python with:
from larajango.config import config, env
name = config("app.name")
debug = env("APP_DEBUG", False)
Framework Package Structure
Larajango keeps old imports such as larajango.routing and larajango.cache working, but the framework package is now organized around clearer Laravel-style boundaries:
larajango/
console/ Artisan-style command application
contracts/ Protocol interfaces for routing, cache, filesystem, queue, auth, config, HTTP
foundation/ Application container and service provider base classes
http/ HTTP factories and adapters
support/ Facades and concrete repositories
templatetags/ Django template tags for routes and Vite
Use contracts for type hints when your application code depends on framework services:
from larajango.contracts.routing import RouterContract
def register_admin_routes(router: RouterContract):
router.get("/admin", AdminController.index, name="admin")
Use service providers to register application services:
from larajango.foundation import ServiceProvider
class AppServiceProvider(ServiceProvider):
def register(self):
self.app.singleton("reports", lambda: ReportService())
Providers are loaded from bootstrap/app.py.
Facades are available from larajango.support:
from larajango.support import Cache, Config, Queue, Route, Storage
name = Config.get("app.name")
Cache.set("key", "value", 60)
path = Storage.disk("public").put("demo.txt", "Hello")
Queue.dispatch(lambda: "done")
URL And Responses
Generate URLs from route names:
from larajango.urls import route
home_url = route("home")
api_url = route("api.health")
Return common response types:
from larajango.responses import back, json, redirect_to, response, view
return json({"ok": True})
return redirect_to("home")
return back(request)
Session
Flash data and retrieve old form input:
from larajango.session import flash, flash_input, flashed, old
flash(request, "status", "Saved.")
message = flashed(request, "status")
Storage
Use local disks similar to Laravel's storage facade:
from larajango.storage import disk
disk("public").put("avatars/user.txt", "stored")
content = disk("public").get("avatars/user.txt")
Expose public storage with:
./artisan storage:link
Authorization And Jobs
Larajango ships with a custom app.User model from startup. It is configured as AUTH_USER_MODEL, migrated in app/migrations/0001_initial.py, and includes convenience helpers over Django's built-in groups and permissions:
from app.Models import Permission, Role, User
user = User.objects.create_user(username="taylor", password="secret")
role = Role.find_or_create("admin")
permission = Permission.find_or_create("publish_posts")
role.give_permission_to(permission)
user.assign_role("admin")
user.give_permission_to("app.view_user")
user.has_role("admin")
user.can("app.view_user")
user.can("view_user")
Protect routes with role or permission middleware:
router.get("/admin", AdminController.index).middleware("role:admin")
router.get("/reports", ReportController.index).middleware("permission:app.view_report")
Define gates:
from larajango.authorization import Gate
Gate.define("update-post", lambda user, post: post.user_id == user.id)
Gate.authorize("update-post", request.user, post)
Policies may be registered in app/Providers/AppServiceProvider.py:
from app.Models.User import User
from app.Policies.UserPolicy import UserPolicy
from larajango.authorization import Gate
Gate.policy(User, UserPolicy)
Gate.authorize("update", request.user, another_user)
Dispatch synchronous jobs:
from app.Jobs.SendWelcomeEmail import SendWelcomeEmail
from larajango.queue import dispatch
dispatch(SendWelcomeEmail())
Pagination And Cache
from larajango.cache import remember
from larajango.pagination import paginate
posts = paginate(request, Post.objects.all(), per_page=10)
stats = remember("stats", 60, lambda: calculate_stats())
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sulaimanqasimi_larjango-0.1.0.tar.gz.
File metadata
- Download URL: sulaimanqasimi_larjango-0.1.0.tar.gz
- Upload date:
- Size: 30.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
bb68413639a3d292c6e2590fe2d428fd689a466550ab3832aa6b036e44b05381
|
|
| MD5 |
824bfe50510b402e592f40fbd07fec6a
|
|
| BLAKE2b-256 |
6bf6fd30187ce5ed3b9be8b983e5ab6efd52bd7ca6780bd7aa3f3c47ebaa1772
|
Provenance
The following attestation bundles were made for sulaimanqasimi_larjango-0.1.0.tar.gz:
Publisher:
workflow.yml on sulaimanQasimi/larjango
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sulaimanqasimi_larjango-0.1.0.tar.gz -
Subject digest:
bb68413639a3d292c6e2590fe2d428fd689a466550ab3832aa6b036e44b05381 - Sigstore transparency entry: 1677566146
- Sigstore integration time:
-
Permalink:
sulaimanQasimi/larjango@600fcde6a4e103fce33d7c5dcef72f9c215e1c12 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/sulaimanQasimi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@600fcde6a4e103fce33d7c5dcef72f9c215e1c12 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file sulaimanqasimi_larjango-0.1.0-py3-none-any.whl.
File metadata
- Download URL: sulaimanqasimi_larjango-0.1.0-py3-none-any.whl
- Upload date:
- Size: 36.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3d9e311cc7201aa77ff2f42ee9b1516f099a7c36ef9db7a0ad3fe7697cc24a9e
|
|
| MD5 |
2184e87fef4abe102b11e3e7c9f8e693
|
|
| BLAKE2b-256 |
b08168a0874821ab7637155c4dce7c07df23f745e4028e913dde98995884ebba
|
Provenance
The following attestation bundles were made for sulaimanqasimi_larjango-0.1.0-py3-none-any.whl:
Publisher:
workflow.yml on sulaimanQasimi/larjango
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sulaimanqasimi_larjango-0.1.0-py3-none-any.whl -
Subject digest:
3d9e311cc7201aa77ff2f42ee9b1516f099a7c36ef9db7a0ad3fe7697cc24a9e - Sigstore transparency entry: 1677566209
- Sigstore integration time:
-
Permalink:
sulaimanQasimi/larjango@600fcde6a4e103fce33d7c5dcef72f9c215e1c12 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/sulaimanQasimi
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@600fcde6a4e103fce33d7c5dcef72f9c215e1c12 -
Trigger Event:
workflow_dispatch
-
Statement type: