@django-next/client
v2.2.0
Published
React client package for Django-Next with authentication, RBAC, file uploads, and seamless integration with generated SDKs
Maintainers
Readme
@django-next/client
React client package for Django-Next with authentication, RBAC, file uploads, and seamless integration with generated SDKs.
🌟 What is this package?
The @django-next/client package provides React components, hooks, and utilities that work seamlessly with the SDK generated by @django-next/cli. It handles:
- Authentication - Login, logout, and session management
- Protected Routes - Role-based access control (RBAC)
- File Uploads - Progress tracking and error handling
- API Context - Seamless integration with generated SDKs
- Error Handling - Comprehensive error boundaries
This package works hand-in-hand with the generated SDK!
📦 Installation
# Install the client package
npm install @django-next/client
# Install required peer dependencies
npm install @tanstack/react-query axios zod
# Don't forget the CLI for generating your SDK
npm install --save-dev @django-next/cli🚀 Features
- 🔐 Authentication - Complete login/logout with session management
- 🛡️ Protected Routes - Role-based access control (RBAC) components
- 📁 File Uploads - Progress tracking and error handling
- 🔗 API Integration - Seamless connection with generated SDKs
- ⚡ React Query - Enhanced query provider and utilities
- 📝 TypeScript - Full type safety throughout
- 🎯 Beginner Friendly - Easy to use with clear examples
🏃♂️ Quick Start
Step 1: Generate your SDK first
# Install and use the CLI to generate your SDK
npm install -g @django-next/cli
django-next init
django-next generateStep 2: Set up providers
// app/providers.tsx
'use client';
import { DjangoNextProvider } from '@django-next/client';
import { ApiClient } from '../.django-next/api'; // Your generated API client
const apiClient = new ApiClient({
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000',
withCredentials: true,
});
export function Providers({ children }: { children: React.ReactNode }) {
return (
<DjangoNextProvider
apiClient={apiClient}
authConfig={{
loginUrl: '/api/auth/login/',
logoutUrl: '/api/auth/logout/',
userUrl: '/api/auth/me/',
refreshUrl: '/api/auth/refresh/',
}}
>
{children}
</DjangoNextProvider>
);
}Step 3: Use in your layout
// app/layout.tsx
import { Providers } from './providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<Providers>
{children}
</Providers>
</body>
</html>
);
}Step 3: Use Generated Hooks
No additional setup needed! The hooks work automatically with DjangoNextProvider.
📖 Usage Examples
Authentication
// components/LoginForm.tsx
import { useAuth } from '@django-next/client';
import { useApi_auth_login_create } from '../.django-next/hooks'; // Generated hook
export function LoginForm() {
const { isAuthenticated, user, logout } = useAuth();
const loginMutation = useApi_auth_login_create({
onSuccess: () => {
console.log('Login successful!');
// Auth state is automatically updated
}
});
if (isAuthenticated) {
return (
<div>
<p>Welcome, {user?.username}!</p>
<button onClick={logout}>Logout</button>
</div>
);
}
const handleLogin = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
loginMutation.mutate({
username: formData.get('username') as string,
password: formData.get('password') as string,
});
};
return (
<form onSubmit={handleLogin}>
<input name="username" placeholder="Username" required />
<input name="password" type="password" placeholder="Password" required />
<button type="submit" disabled={loginMutation.isPending}>
{loginMutation.isPending ? 'Logging in...' : 'Login'}
</button>
{loginMutation.error && (
<p style={{ color: 'red' }}>Error: {loginMutation.error.message}</p>
)}
</form>
);
}Protected Routes & RBAC
// components/AdminPanel.tsx
import { Protected } from '@django-next/client';
import { useApi_users_list } from '../lib/api/hooks'; // Generated hook
export function AdminPanel() {
return (
<Protected
hasAll={["users.view_user"]}
fallback={<div>Access denied. Admin permissions required.</div>}
>
<AdminContent />
</Protected>
);
}
function AdminContent() {
const { data: users, isLoading } = useUsersList();
return (
<div>
<h1>Admin Panel</h1>
{/* Only show edit button if user has permission */}
<Protected hasAll={["users.change_user"]}>
<button>Edit Users</button>
</Protected>
{/* Only show for staff members */}
<RequireStaff>
<button>Staff Only Feature</button>
</RequireStaff>
{/* Multiple permissions required */}
<Protected hasAll={["users.add_user", "users.delete_user"]}>
<button>Advanced User Management</button>
</Protected>
</div>
);
}File Uploads
// components/FileUpload.tsx
import { useApi_files_create } from '../lib/api/hooks'; // Generated hook
import { useState } from 'react';
export function FileUpload() {
const [file, setFile] = useState<File | null>(null);
const [progress, setProgress] = useState(0);
const uploadMutation = useApi_files_create({
onUploadProgress: (progressValue) => {
setProgress(progressValue);
},
onSuccess: () => {
setFile(null);
setProgress(0);
alert('File uploaded successfully!');
}
});
const handleUpload = () => {
if (!file) return;
uploadMutation.mutate({
file,
title: file.name,
description: 'Uploaded via Django-Next'
});
};
return (
<div>
<input
type="file"
onChange={(e) => setFile(e.target.files?.[0] || null)}
/>
{file && (
<div>
<p>Selected: {file.name}</p>
<button onClick={handleUpload} disabled={uploadMutation.isPending}>
{uploadMutation.isPending ? 'Uploading...' : 'Upload'}
</button>
{uploadMutation.isPending && (
<div>
<div>Progress: {progress}%</div>
<progress value={progress} max={100} />
</div>
)}
</div>
)}
</div>
);
}🔧 API Reference
DjangoNextProvider
Provides unified API client, authentication state, and React Query configuration throughout your app.
import { useAuth } from '@django-next/client';
const {
isAuthenticated, // boolean
user, // User object or null
isLoading, // boolean
login, // (credentials) => Promise
logout, // () => Promise
checkPermission, // (permission) => boolean
hasRole, // (role) => boolean
error // Error object or null
} = useAuth();Protected Component
Conditionally render content based on authentication and permissions.
<Protected
hasAll={["posts.view_post"]} // User must have ALL specified permissions
hasAnyRole={["admin", "moderator"]} // User must have AT LEAST ONE of the specified roles
requireAllPermissions={true} // Require ALL permissions (default: any)
fallback={<div>Access denied</div>} // What to show when access denied
onAccessDenied={() => console.log('Access denied')} // Callback
>
<YourProtectedContent />
</Protected>Backend Security Checklist (for Django with Simple JWT)
To ensure secure authentication and session management, configure your Django backend as follows:
- JWT in httpOnly Cookies: Issue JWT access and refresh tokens only in
httpOnly,Secure,SameSite=Strictcookies. Do not expose tokens in localStorage or headers. - CSRF Protection: Enable Django's CSRF middleware. Rotate CSRF tokens on login/logout and ensure the client updates the token.
- Token Expiry Handling: On refresh token expiry or invalidation, return a clear error so the client can log out the user.
- Secure Cookie Flags: Always set cookies with
Secure,HttpOnly, andSameSite=Strictin production. - No Sensitive Data in LocalStorage: Never store sensitive tokens or user info in localStorage/sessionStorage.
- CORS: Configure CORS to only allow trusted origins and support credentials.
- Session Logout: Invalidate refresh tokens on logout and clear cookies on both client and server.
Django JWT HTTP-Only Cookie Setup
Since Simple JWT doesn't support HTTP-only cookies by default, you need to create custom views and authentication. Here's the complete setup:
1. Install Required Packages
pip install djangorestframework
pip install djangorestframework-simplejwt
pip install django-cors-headers # if you need CORS2. Django Settings Configuration
# settings.py
import os
from datetime import timedelta
IS_LOCAL = os.environ.get("DJANGO_ENV") == "local"
INSTALLED_APPS = [
# ... your other apps
'rest_framework',
'rest_framework_simplejwt',
'rest_framework_simplejwt.token_blacklist', # For secure logout
'corsheaders', # if using CORS
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware', # if using CORS
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
# Django REST Framework settings
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'your_app.authentication.JWTCookieAuthentication', # Your custom auth class
],
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
}
# Simple JWT settings
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True, # Generate new refresh token on refresh
'BLACKLIST_AFTER_ROTATION': True, # Blacklist old refresh tokens
}
# Custom JWT Cookie settings
JWT_COOKIE_NAME = 'access_token'
JWT_REFRESH_COOKIE_NAME = 'refresh_token'
JWT_COOKIE_SECURE = not IS_LOCAL # Secure only in production
JWT_COOKIE_HTTP_ONLY = True
JWT_COOKIE_SAME_SITE = 'Strict'
# Standard Django cookie settings
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Strict"
SESSION_COOKIE_SECURE = not IS_LOCAL
CSRF_COOKIE_HTTPONLY = True
CSRF_COOKIE_SAMESITE = "Strict"
CSRF_COOKIE_SECURE = not IS_LOCAL
# CORS settings (if frontend and backend are on different ports/domains)
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOWED_ORIGINS = [
"http://localhost:3000", # Frontend dev server
"https://your-production-domain.com",
]3. Create Custom Authentication Class
Create your_app/authentication.py:
# authentication.py
from rest_framework_simplejwt.authentication import JWTAuthentication
from django.conf import settings
class JWTCookieAuthentication(JWTAuthentication):
"""
Custom JWT authentication that reads tokens from HTTP-only cookies
instead of Authorization headers
"""
def authenticate(self, request):
# Get token from cookie
raw_token = request.COOKIES.get(settings.JWT_COOKIE_NAME)
if raw_token is None:
return None
# Validate token using Simple JWT's built-in validation
validated_token = self.get_validated_token(raw_token)
user = self.get_user(validated_token)
return (user, validated_token)4. Create Custom Views for Cookie Management
Create your_app/views.py:
# views.py
from rest_framework import status
from rest_framework.response import Response
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from rest_framework_simplejwt.tokens import RefreshToken
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from django.conf import settings
class CookieTokenObtainPairView(TokenObtainPairView):
"""
Login view that sets JWT tokens in HTTP-only cookies
"""
def post(self, request, *args, **kwargs):
response = super().post(request, *args, **kwargs)
if response.status_code == 200:
access_token = response.data['access']
refresh_token = response.data['refresh']
# Remove tokens from response body for security
response.data = {'message': 'Login successful'}
# Set access token cookie
response.set_cookie(
settings.JWT_COOKIE_NAME,
access_token,
max_age=settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'].total_seconds(),
httponly=settings.JWT_COOKIE_HTTP_ONLY,
secure=settings.JWT_COOKIE_SECURE,
samesite=settings.JWT_COOKIE_SAME_SITE,
)
# Set refresh token cookie
response.set_cookie(
settings.JWT_REFRESH_COOKIE_NAME,
refresh_token,
max_age=settings.SIMPLE_JWT['REFRESH_TOKEN_LIFETIME'].total_seconds(),
httponly=settings.JWT_COOKIE_HTTP_ONLY,
secure=settings.JWT_COOKIE_SECURE,
samesite=settings.JWT_COOKIE_SAME_SITE,
)
return response
class CookieTokenRefreshView(TokenRefreshView):
"""
Token refresh view that handles HTTP-only cookies
"""
def post(self, request, *args, **kwargs):
# Get refresh token from cookie
refresh_token = request.COOKIES.get(settings.JWT_REFRESH_COOKIE_NAME)
if not refresh_token:
return Response(
{'error': 'Refresh token not found'},
status=status.HTTP_401_UNAUTHORIZED
)
# Add to request data for parent class processing
request.data['refresh'] = refresh_token
response = super().post(request, *args, **kwargs)
if response.status_code == 200:
access_token = response.data['access']
response.data = {'message': 'Token refreshed'}
# Set new access token cookie
response.set_cookie(
settings.JWT_COOKIE_NAME,
access_token,
max_age=settings.SIMPLE_JWT['ACCESS_TOKEN_LIFETIME'].total_seconds(),
httponly=settings.JWT_COOKIE_HTTP_ONLY,
secure=settings.JWT_COOKIE_SECURE,
samesite=settings.JWT_COOKIE_SAME_SITE,
)
return response
@api_view(['POST'])
@permission_classes([AllowAny])
def logout_view(request):
"""
Logout view that clears HTTP-only cookies and blacklists refresh token
"""
refresh_token = request.COOKIES.get(settings.JWT_REFRESH_COOKIE_NAME)
# Blacklist the refresh token for security
if refresh_token:
try:
token = RefreshToken(refresh_token)
token.blacklist()
except Exception:
pass # Token might already be invalid
response = Response({'message': 'Logged out successfully'})
# Clear both cookies
response.delete_cookie(settings.JWT_COOKIE_NAME)
response.delete_cookie(settings.JWT_REFRESH_COOKIE_NAME)
return response5. Configure URLs
In your urls.py:
# urls.py
from django.urls import path
from .views import CookieTokenObtainPairView, CookieTokenRefreshView, logout_view
urlpatterns = [
# Replace default Simple JWT URLs with cookie versions
path('api/token/', CookieTokenObtainPairView.as_view(), name='login'),
path('api/token/refresh/', CookieTokenRefreshView.as_view(), name='refresh'),
path('api/token/logout/', logout_view, name='logout'),
# ... your other URLs
]Setup
Recommended Setup (Using createDjangoClient)
The recommended way to set up the client is using createDjangoClient() which ensures proper configuration:
// lib/api-client.ts
import { createDjangoClient } from '@django-next/client';
import { ApiClient } from './api/api'; // Your generated API client
export const { api, axiosInstance, config } = createDjangoClient({
baseUrl: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000',
apiClass: ApiClient,
auth: {
loginUrl: '/api/auth/login/',
logoutUrl: '/api/auth/logout/',
userUrl: '/api/auth/me/',
refreshUrl: '/api/auth/refresh/',
},
timeout: 30000,
withCredentials: true,
onError: (error) => console.error('API Error:', error),
});Alternative Setup (Direct API Client)
If you prefer to use the API client directly, make sure to provide a baseURL:
// lib/api-client.ts
import { ApiClient } from './api/api';
export const api = new ApiClient({
baseURL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000',
timeout: 30000,
withCredentials: true,
auth: {
loginUrl: '/api/auth/login/',
logoutUrl: '/api/auth/logout/',
userUrl: '/api/auth/me/',
refreshUrl: '/api/auth/refresh/',
},
});⚠️ Important: Always provide a baseURL when creating the API client directly, otherwise authentication and API calls may fail.
Quick Start
- Generate the SDK using the CLI:
pnpm dlx @django-next/cli generate - Set up the client using one of the methods above.
- Import and use the generated files in your Next.js app.
Usage Examples
API Client
import { ApiClient } from './.django-next/api';
const api = new ApiClient();
const data = await api.someEndpoint(params);React Query Hooks
import { useSomeEndpoint } from './.django-next/hooks';
const { data, isLoading, error, isError } = useSomeEndpoint(params);
if (isError) {
// Handle or display error.message
}DjangoNextProvider & useAuth
import { DjangoNextProvider, useAuth } from '@django-next/client';
<DjangoNextProvider apiClient={apiClient}>
<YourApp />
</DjangoNextProvider>Protected (RBAC)
import { Protected } from '@django-next/client';
<Protected hasAll={['admin']} fallback={<div>Access denied</div>}>
<AdminPanel />
</Protected>File Upload
import { uploadFile } from '@django-next/client/upload-file';
await uploadFile(api.axios, '/api/upload/', file, { extraField: 'value' }, {
onProgress: (percent) => console.log(`Upload: ${percent}%`)
});Batch API Calls
import { batchApiCalls } from '@django-next/client/batch-api-calls';
// Atom mode (default): fail on first error
try {
const results = await batchApiCalls([
() => api.getUser({ id: 1 }),
() => api.getPosts({ page: 1 }),
]);
} catch (err) {
// Handle/log error, inspect which call failed
}
// Non-atom mode: get all results/errors
const results = await batchApiCalls([
() => api.getUser({ id: 1 }),
() => api.getPosts({ page: 1 }),
], { atom: false });
results.forEach((res, i) => {
if ('error' in res) {
// Handle error for call i
} else {
// Use result for call i
}
});Batch Query Hook
import { useBatchQuery } from '@django-next/client/use-batch-query';
const { data, isLoading, error } = useBatchQuery([
{ queryKey: ['user'], queryFn: () => api.getUser({ id: 1 }) },
{ queryKey: ['posts'], queryFn: () => api.getPosts({ page: 1 }) },
]);
if (error) {
// Show error message in UI
}Server Actions (Next.js)
import { someEndpointAction } from './.django-next/actions';
export async function action(formData) {
try {
return await someEndpointAction(formData);
} catch (err) {
// Handle/log error, return custom error response, etc.
return { error: err instanceof Error ? err.message : String(err) };
}
}Error Handling
- Hooks: Use
error/isErrorfrom React Query hooks to display or handle errors in your UI. - Batch:
- With
atom: true(default): use try/catch to handle the first error. - With
atom: false: inspect each result; errors are returned as{ error: string }objects.
- With
- Server Actions: Always wrap generated actions in try/catch to handle errors gracefully. Actions return
{ error: string }on failure.
Troubleshooting
Common Configuration Issues
Authentication requests go to wrong URL (e.g., /auth instead of http://localhost:8000/api/auth/login/)
Problem: Login requests are being sent to relative paths without the base URL.
Solution: Ensure you're providing a baseURL when creating the API client:
// ❌ Wrong - missing baseURL
const api = new ApiClient();
// ✅ Correct - with baseURL
const api = new ApiClient({
baseURL: 'http://localhost:8000',
});
// ✅ Better - use createDjangoClient
const { api } = createDjangoClient({
baseUrl: 'http://localhost:8000',
apiClass: ApiClient,
});DjangoNextProvider throws "axios instance not found" error
Problem: The API client doesn't have a properly configured axios instance.
Solution: Use createDjangoClient() or ensure your API client has the _config property set up correctly.
Configuration not being applied
Problem: Auth URLs or other configuration options are not being used.
Solution: Make sure you're passing the configuration to the right place:
// For createDjangoClient
const { api } = createDjangoClient({
baseUrl: 'http://localhost:8000',
apiClass: ApiClient,
auth: {
loginUrl: '/api/auth/login/',
// ... other auth config
},
});
// For direct API client usage
const api = new ApiClient({
baseURL: 'http://localhost:8000',
auth: {
loginUrl: '/api/auth/login/',
// ... other auth config
},
});Other Common Issues
- Type errors: Re-run codegen to sync with your API schema.
- Auth/session issues: Check your Django backend and config.
- File upload issues: Ensure your endpoint accepts
multipart/form-data.
For more, see the generated SDK docs in .django-next/ after running the CLI.
