django-api-starter
Generate a production-ready Django REST API project in one command.
Every new API needs the same foundation: a custom user model, JWT login,
profile and password endpoints, settings per environment, Docker and tests.
django-api-starter writes all of it for you, with a flat structure you can
read in one sitting. You choose the extras: email two-factor authentication,
Celery with Redis, PostgreSQL or SQLite, Docker, and API docs.
pip install django-api-starter
django-starter new myproject
Contents
- Features
- Requirements
- Quick start
- Command reference
- Generated project
- API reference
- Email verification and two-factor login
- Background tasks with Celery and Redis
- Configuration
- Security defaults
- Testing
- Deployment
- Customizing the project
- Troubleshooting
- Roadmap
- License
Features
Always included
- Custom user model that logs in with email: name, phone, profile picture, role (
admin/user), active flag and timestamps. It is set up before the first migration, as Django recommends. - JWT authentication with Simple JWT: short-lived access tokens, and refresh tokens that rotate and can't be reused.
- Account APIs: register, login, logout, token refresh, profile read and update (including picture upload), change password, and forgotten-password reset by email.
- Admin user management: an admin-only list with search, ordering and pagination, where admins can change a user's role or deactivate them.
- Settings per environment:
development,productionandtest, all configured from a.envfile. - One error format for every endpoint, so frontends handle errors in one place.
- Rate limiting on the login, signup and reset endpoints.
- Extras: CORS, request IDs for tracing, a
/health/endpoint, await_for_dbcommand, and Django admin set up for the custom user. - A pytest suite that covers every endpoint.
Optional
| Option | Adds |
|---|---|
--2fa |
Email one-time codes: signup verifies the email address, and users can require a code at every login |
--celery |
Celery worker and scheduler with Redis. Emails are sent in the background, and clean-up jobs run on a schedule |
--database postgres |
PostgreSQL driver, DATABASE_URL and a Docker database service |
--docker |
Dockerfile for production and docker-compose.yml for local development |
--api-docs |
OpenAPI schema and Swagger UI with drf-spectacular |
Requirements
| To... | You need |
|---|---|
| Run the generator | Python 3.10+ |
| Run a generated project | Python 3.12+ (Django 6.1), or Docker |
Use --database postgres without Docker |
A PostgreSQL server |
Use --celery without Docker |
A Redis server |
| Send real emails | An SMTP account (Gmail, SendGrid, Amazon SES, Mailgun, and so on) |
With Docker, PostgreSQL and Redis run in containers, so you only need Docker installed.
Quick start
Option 1: SQLite, no Docker
This is the fastest way to try it. You only need Python 3.12+.
pip install django-api-starter
django-starter new myproject --database sqlite --no-docker -y
cd myproject
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements/development.txt
python manage.py migrate
python manage.py createsuperuser
python manage.py runserver
Open http://localhost:8000/api/docs/ to try the endpoints, and http://localhost:8000/admin/ for the Django admin. In development, emails (reset links and codes) are printed to the terminal.
Option 2: Docker (PostgreSQL, Redis, Celery)
pip install django-api-starter
django-starter new myproject --2fa --celery -y
cd myproject
docker compose up --build
docker compose exec web python manage.py createsuperuser
This starts the API on port 8000 together with PostgreSQL, Redis, a Celery worker and the Celery scheduler. Your code is mounted into the containers, and the server reloads when you save a file.
Option 3: Interactive
django-starter new myproject
Database (postgres/sqlite) [postgres]
Enable email OTP / two-factor authentication? [y/N]
Add Celery background tasks with Redis? [y/N]
Add Docker support? [Y/n]
Add API documentation (Swagger)? [Y/n]
Press Enter to accept the default shown in brackets.
Command reference
django-starter new NAME [DIRECTORY] [options]
The command is also available as django-api-starter, and
python -m django_api_starter works too.
| Argument / option | Default | Description |
|---|---|---|
NAME |
Project name. Letters, digits, - and _, starting with a letter. No spaces |
|
DIRECTORY |
./NAME |
Where to create the project. The folder must be empty or not exist yet |
--database {postgres,sqlite} |
postgres |
Database backend |
--2fa / --no-2fa |
off | Email verification and two-factor login |
--celery / --no-celery |
off | Celery background tasks with Redis |
--docker / --no-docker |
on | Docker files |
--api-docs / --no-api-docs |
on | OpenAPI schema and Swagger UI |
-y, --yes |
Don't ask questions; use defaults for options you didn't give | |
--version |
Show the version |
Examples:
# Everything on
django-starter new shop --2fa --celery -y
# Minimal: SQLite, no Docker, no API docs
django-starter new shop --database sqlite --no-docker --no-api-docs -y
# Into a specific folder
django-starter new shop ~/projects/shop-api -y
Generated project
myproject/
├── apps/
│ └── accounts/
│ ├── migrations/
│ │ └── 0001_initial.py
│ ├── admin.py # Django admin for users (and codes)
│ ├── apps.py
│ ├── models.py # User (and OTP)
│ ├── serializers.py # Validation for every endpoint
│ ├── tasks.py # Scheduled clean-up tasks (--celery)
│ ├── urls.py
│ └── views.py # Every account endpoint
├── conf/
│ ├── settings/
│ │ ├── base.py # Shared settings, read from .env
│ │ ├── development.py # DEBUG on, emails printed to the console
│ │ ├── production.py # HTTPS, secure cookies, HSTS
│ │ └── test.py # Fast hashing, in-memory email
│ ├── celery.py # Celery app (--celery)
│ ├── asgi.py
│ ├── urls.py
│ └── wsgi.py
├── core/ # Code shared by every app
│ ├── management/commands/
│ │ └── wait_for_db.py
│ ├── auth.py # Issue and revoke JWTs
│ ├── exceptions.py # The shared error format
│ ├── factories.py # Test data factories
│ ├── middleware.py # Request IDs
│ ├── pagination.py
│ ├── permissions.py # IsAdmin, IsOwnerOrAdmin
│ ├── tasks.py # Background email sending (--celery)
│ ├── utils.py # send_templated_email()
│ └── views.py # /health/
├── requirements/
│ ├── base.txt
│ ├── development.txt
│ └── production.txt
├── templates/emails/ # password_reset.html (and otp.html)
├── tests/
│ ├── conftest.py
│ └── accounts/ # auth, profile, password (otp, tasks)
├── .env # Your local settings, with a random SECRET_KEY
├── .env.example # Every setting, documented
├── Dockerfile # (--docker)
├── docker-compose.yml # (--docker)
├── manage.py
├── pytest.ini
└── README.md # Docs for the generated project
Files marked with an option are only created when you choose it. The
accounts app keeps its models, serializers and views in one file each, so you
can follow any endpoint from URL to database without jumping between folders.
The user model
| Field | Type | Notes |
|---|---|---|
email |
email, unique | Used to log in. Saved in lowercase |
name |
text | Required |
phone |
text | Optional |
profile_picture |
image | Optional. Saved under a random file name |
role |
admin / user |
Application role; defaults to user. Users can't change their own role |
is_active |
boolean | Inactive users can't log in, and their tokens stop working |
is_staff, is_superuser |
boolean | Access to the Django admin, separate from role |
is_email_verified |
boolean | --2fa only |
two_factor_enabled |
boolean | --2fa only. Asks for a code at every login |
date_joined, updated_at, last_login |
date and time | Set automatically |
The password is hashed by Django and never returned by the API.
API reference
All endpoints are under /api/v1/. Send the access token in the header
Authorization: Bearer <access>.
| Method | Endpoint | Login required | Description |
|---|---|---|---|
| POST | auth/register/ |
no | Create an account |
| POST | auth/login/ |
no | Log in with email and password |
| POST | auth/logout/ |
yes | Revoke a refresh token |
| POST | auth/token/refresh/ |
no | Exchange a refresh token for new tokens |
| POST | auth/otp/verify/ |
no | Verify an emailed code and receive tokens (--2fa) |
| POST | auth/otp/resend/ |
no | Send a new code (--2fa) |
| GET | auth/me/ |
yes | Your profile |
| PATCH | auth/me/ |
yes | Update your name, phone or picture (or two_factor_enabled) |
| POST | auth/password/change/ |
yes | Change your password |
| POST | auth/password/reset/ |
no | Email a password reset link |
| POST | auth/password/reset/confirm/ |
no | Set a new password with the link's uid and token |
| GET | users/ |
admin | List users. Supports ?search=, ?ordering=, ?page= and ?page_size= |
| GET | users/<id>/ |
admin | Get one user |
| PATCH | users/<id>/ |
admin | Change a user's details, role or is_active |
Examples
Register
curl -X POST http://localhost:8000/api/v1/auth/register/ \
-H "Content-Type: application/json" \
-d '{"name": "Jane Doe", "email": "jane@example.com", "phone": "+15550100",
"password": "Sup3r-Secret-pass", "password_confirm": "Sup3r-Secret-pass"}'
{
"user": {"id": 1, "email": "jane@example.com", "name": "Jane Doe", "role": "user", "...": "..."},
"tokens": {"access": "eyJ...", "refresh": "eyJ..."}
}
With --2fa, the response has no tokens yet. It asks for the code sent by email
(see below).
Log in
curl -X POST http://localhost:8000/api/v1/auth/login/ \
-H "Content-Type: application/json" \
-d '{"email": "jane@example.com", "password": "Sup3r-Secret-pass"}'
The response has the same shape as registration: user and tokens.
Refresh tokens
curl -X POST http://localhost:8000/api/v1/auth/token/refresh/ \
-H "Content-Type: application/json" \
-d '{"refresh": "eyJ..."}'
This returns a new access and a new refresh token. The old refresh token
stops working, so always store the new one.
Update your profile, including a picture
curl -X PATCH http://localhost:8000/api/v1/auth/me/ \
-H "Authorization: Bearer eyJ..." \
-F name="Jane Smith" \
-F profile_picture=@photo.jpg
Use JSON for text fields only, or multipart/form-data when you upload a picture.
Reset a forgotten password
POST auth/password/reset/with{"email": "jane@example.com"}. The response is the same whether or not the account exists.- The email links to
PASSWORD_RESET_URL, your frontend page, withuidandtokenadded to it. - Your frontend sends
POST auth/password/reset/confirm/with{"uid": "...", "token": "...", "new_password": "...", "new_password_confirm": "..."}.
The link works once and expires after PASSWORD_RESET_TIMEOUT seconds (1 hour by default).
Errors
Every error uses the same shape:
{
"message": "Passwords do not match.",
"code": "invalid",
"errors": {"password_confirm": ["Passwords do not match."]}
}
messageis a human-readable summary you can show directly.codeis stable text your code can check, such asinvalid,not_authenticated,permission_denied,throttledortoken_not_valid.errorslists messages per field. It is only present for validation errors (status 400).
Email verification and two-factor login
Generate with --2fa to turn this on.
register ──► code emailed ──► otp/verify ──► tokens
▲
login ──► password OK ──┬── email not verified ──► code emailed ─┤
├── 2FA turned on ───────► code emailed ─┘
└── otherwise ──────────► tokens
-
Register. The account is created and a code is emailed. The response has no tokens:
{ "message": "A verification code was sent to jane@example.com.", "requires_otp": true, "verification_id": "0b6f6f4e-...", "purpose": "email_verification", "expires_at": "2026-09-25T10:15:00Z" }
-
Verify.
POST auth/otp/verify/with{"verification_id": "...", "code": "123456"}marks the email as verified and returnsuserandtokens. -
Log in. If the email isn't verified yet, or the user has turned on
two_factor_enabled, login returns a challenge like the one in step 1 instead of tokens. Verify it the same way. -
Resend.
POST auth/otp/resend/with{"verification_id": "..."}sends a new code and returns a newverification_id. The old code stops working. -
Turn 2FA on or off.
PATCH auth/me/with{"two_factor_enabled": true}.
How codes are protected:
- Only a keyed hash of each code is stored, never the code itself.
- Each code works once and expires after
OTP_EXPIRY_MINUTES(10 by default). - After
OTP_MAX_ATTEMPTSwrong tries (5 by default), the code is locked and the user must request a new one. - Resending has a cooldown of
OTP_RESEND_COOLDOWN_SECONDS(60 by default). - No token is issued until the code is verified.
Background tasks with Celery and Redis
Generate with --celery to turn this on. Redis is used for three things:
- Celery broker: the queue that carries tasks to the worker.
- Task results, kept for one day.
- Django's cache. Rate limits are counted in the cache, so with Redis they are shared by all server processes. With the default in-memory cache, each process would keep its own counters.
What runs in the background
| Task | When | What it does |
|---|---|---|
core.tasks.send_email |
Whenever an email is sent | Sends the email. Retries up to 5 times with increasing delays if the mail server fails. API requests don't wait for it |
flush_expired_tokens |
Daily | Deletes expired refresh tokens from the database |
delete_stale_otps |
Hourly (--2fa only) |
Deletes used and expired codes |
Running it without Docker (Redis must be running at REDIS_URL):
celery -A conf worker -l info # Windows: add --pool=solo
celery -A conf beat -l info # runs the scheduled tasks
Adding your own task. Create tasks.py in any app; Celery finds it
automatically:
from celery import shared_task
@shared_task
def generate_report(report_id):
...
generate_report.delay(report_id=42) # returns immediately
Pass IDs and plain values to tasks, not model instances. To run a task on a
schedule, add it to CELERY_BEAT_SCHEDULE in conf/settings/base.py.
The test settings run tasks immediately and use an in-memory cache, so pytest
doesn't need Redis.
Configuration
Settings are read from environment variables or the .env file. The generator
creates .env with a random SECRET_KEY. .env.example lists every setting and
should be committed; .env is git-ignored and should not be.
| Variable | Default | Description |
|---|---|---|
SECRET_KEY |
(random in .env) |
Required in production |
DEBUG |
False |
Always True in development settings and False in production |
ALLOWED_HOSTS |
localhost,127.0.0.1 |
Required in production |
DATABASE_URL |
SQLite file | For example postgres://user:pass@host:5432/db |
DB_CONN_MAX_AGE |
60 |
Seconds to keep database connections open |
CORS_ALLOWED_ORIGINS |
none | Frontend origins allowed to call the API, comma-separated |
CSRF_TRUSTED_ORIGINS |
none | Production only; for the admin behind HTTPS |
JWT_ACCESS_TOKEN_LIFETIME_MINUTES |
15 |
|
JWT_REFRESH_TOKEN_LIFETIME_DAYS |
30 |
|
EMAIL_BACKEND |
SMTP (console in development) | |
EMAIL_HOST, EMAIL_PORT, EMAIL_HOST_USER, EMAIL_HOST_PASSWORD, EMAIL_USE_TLS, EMAIL_TIMEOUT |
SMTP settings | |
DEFAULT_FROM_EMAIL |
<project> <noreply@example.com> |
Sender address |
PASSWORD_RESET_URL |
http://localhost:3000/reset-password?uid={uid}&token={token} |
Your frontend's reset page |
PASSWORD_RESET_TIMEOUT |
3600 |
Reset link lifetime in seconds |
THROTTLE_AUTH_RATE |
10/min |
Rate limit for login, signup and reset, per IP address |
THROTTLE_OTP_RATE |
10/min |
Rate limit for code verify and resend (--2fa) |
OTP_LENGTH, OTP_EXPIRY_MINUTES, OTP_MAX_ATTEMPTS, OTP_RESEND_COOLDOWN_SECONDS |
6, 10, 5, 60 |
--2fa |
REDIS_URL |
redis://localhost:6379/0 |
--celery |
CELERY_BROKER_URL, CELERY_RESULT_BACKEND |
REDIS_URL |
--celery. Only set these to use something other than Redis |
TIME_ZONE |
UTC |
|
LOG_LEVEL |
INFO |
|
SECURE_SSL_REDIRECT |
True |
Production |
SECURE_HSTS_SECONDS |
30 days | Production |
manage.py uses conf.settings.development by default. wsgi.py and asgi.py
use conf.settings.production. Set DJANGO_SETTINGS_MODULE to override either.
Security defaults
- Passwords are hashed by Django and checked against Django's password validators.
- Access tokens last 15 minutes. Refresh tokens rotate, and a used one can't be reused.
- Logout revokes the refresh token, and a user can only log out their own token.
- Changing or resetting a password logs the user out everywhere.
- Deactivated users can't log in, and their existing tokens are rejected.
- Login gives the same error for a wrong password, an unknown email and an inactive account, so accounts can't be discovered.
- Password reset responds the same way whether or not the email is registered.
- Login, signup, reset and code endpoints are rate-limited per IP address.
- Users can't set their own role, email or active status through the API.
- Uploaded pictures get random file names.
- Production settings require
SECRET_KEYandALLOWED_HOSTS, and turn on HTTPS redirect, secure cookies and HSTS. - The Docker image runs as a non-root user.
Testing
pytest
The suite covers registration, login, token rotation, logout, profile updates
(including picture upload), admin permissions, password change and reset, and
every code and task path when those options are on. It uses fast password
hashing and in-memory email, so it runs in a few seconds. It needs no Redis;
it only needs PostgreSQL if DATABASE_URL points to it.
Use core.factories.UserFactory to create test users:
from core.factories import UserFactory
admin = UserFactory(role="admin")
Deployment
- Set
DJANGO_SETTINGS_MODULE=conf.settings.production. - Set
SECRET_KEY,ALLOWED_HOSTS,DATABASE_URL,CORS_ALLOWED_ORIGINS, the email settings, andREDIS_URLif you use Celery. - Run
python manage.py migrateon each deploy. - Serve with gunicorn:
gunicorn conf.wsgi:application --bind 0.0.0.0:8000. TheDockerfilealready does this, and runscollectstaticwhen the image is built. - With Celery, run the same image twice more with the commands
celery -A conf worker -l infoandcelery -A conf beat -l info. Run only one beat process, or scheduled tasks will run more than once. - Put the app behind a proxy or load balancer that handles HTTPS and sets
X-Forwarded-Proto.
Build the production image with:
docker build -t myproject .
Customizing the project
The generated code is yours to change. It has no runtime dependency on
django-api-starter. Common changes:
- Add a role. Add it to
User.Roleinapps/accounts/models.py, runpython manage.py makemigrations, and userequest.user.rolein a permission class incore/permissions.py. - Add a field to users. Add it to the model, run
makemigrations, and list it inUserSerializer.Meta.fields(and inread_only_fieldsif users shouldn't edit it). - Add an app. Run
mkdir apps/ordersthenpython manage.py startapp orders apps/orders. In itsapps.py, setname = "apps.orders", then add"apps.orders"toLOCAL_APPSand include its URLs inconf/urls.py. - Change token lifetimes. Set
JWT_ACCESS_TOKEN_LIFETIME_MINUTESandJWT_REFRESH_TOKEN_LIFETIME_DAYSin.env. - Restyle emails. Edit the HTML files in
templates/emails/. The plain-text version is created from the HTML automatically. - Switch email provider. Use any SMTP service by setting the
EMAIL_*variables, or setEMAIL_BACKENDto a provider's Django backend.
Troubleshooting
django-api-starter: command not found
Use django-starter, or upgrade with pip install --upgrade django-api-starter;
the latest version accepts both names. If
neither is found, pip's scripts folder isn't on your PATH. Run
python -m django_api_starter new myproject instead.
error: unrecognized arguments
Project names can't contain spaces: new home care reads home as the name and
care as the folder. Use home_care or home-care.
Target directory ... is not empty
The generator never writes into a folder that already has files, so it can't
overwrite your work. Leave out the directory to create a new folder, or pass an
empty folder.
Django fails to install, or says it needs a newer Python Generated projects use Django 6.1, which needs Python 3.12+. Upgrade Python or use Docker.
Error loading psycopg2 or psycopg module
The project uses PostgreSQL, but the driver isn't installed. Run
pip install -r requirements/development.txt inside the project's virtual
environment.
Connection refused on port 6379, or /health/ reports the cache as unavailable
Redis isn't running. Start it with docker compose up -d redis, or install Redis
locally and check REDIS_URL.
The Celery worker exits or hangs on Windows
Celery's default worker pool doesn't support Windows. Run
celery -A conf worker -l info --pool=solo.
I don't see the OTP or reset email
In development, emails are printed to the console: the runserver terminal, or
the Celery worker's terminal with --celery. Set the EMAIL_* variables to send
real emails.
401 with token_not_valid on refresh
Each refresh token works once. Store the new refresh token from every refresh
response.
Roadmap
- SMS codes
- Authenticator app (TOTP) codes
- Social login (Google, GitHub, Apple)
- Listing and ending active sessions
- More storage options, such as Amazon S3
Suggestions and pull requests are welcome; see CONTRIBUTING.md.
License
Release files for django-api-starter 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_api_starter-0.2.0.tar.gz | 53.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_api_starter-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 112.8 kB
Release files / django_api_starter-0.2.0.tar.gz
| Download URL | django_api_starter-0.2.0.tar.gz |
|---|---|
| Size | 53.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
415837ede1d14f5a4441fd23c1a8853bf81331f812a4e8efeb3acb879c011fcb
|
|
BLAKE2b-256 checksum How to use checksums |
e5adb0fb46c6a7e8f5091d96c8a359525250036dca1e5509b011c11ced5d72d9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency logRelease files / django_api_starter-0.2.0-py3-none-any.whl
| Download URL | django_api_starter-0.2.0-py3-none-any.whl |
|---|---|
| Size | 59.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
703a1772812f86555531491a52dfae79bdfed356b2a3aba2bdb69a011dfc2a8e
|
|
BLAKE2b-256 checksum How to use checksums |
7e2ed47e8a9ef8ceaaff22315fb1075eee91cf0f82a5d70e8421efa228c1f92d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency log