A simple full-stack application demonstrating JWT (JSON Web Token) authentication with Express API, React web frontend, and React Native mobile app. Perfect for teaching students the basics of token-based authentication across different platforms.
This is a monorepo using npm workspaces:
JWT-Token-Example/
├── api/ # Express backend
│ ├── middleware/ # Authentication middleware
│ ├── routes/ # API routes
│ ├── server.js # Main server file
│ └── .env # Environment variables
├── client/ # React web frontend
│ ├── src/
│ │ ├── components/ # React components
│ │ ├── App.jsx # Main App component
│ │ └── main.jsx # Entry point
│ └── index.html
├── mobile/ # React Native mobile app (Expo)
│ ├── src/
│ │ ├── screens/ # Mobile screens
│ │ └── services/ # API services
│ ├── App.js # Main mobile app
│ └── app.json # Expo configuration
├── documentation/ # Educational documentation
│ └── conceptos-jwt.md # JWT concepts in Spanish
└── package.json # Root workspace configuration
- Node.js (v16 or higher)
- npm (v7 or higher)
- For mobile: Expo Go app on your phone OR iOS Simulator/Android Emulator
- Install all dependencies (API, web client, and mobile):
npm installThis will install dependencies for the root workspace and all three workspaces (API, client, and mobile).
You have several options:
Option 1: Run both API and web client together (recommended)
npm run devThis will start:
- API server on http://localhost:5001
- React web client on http://localhost:3000
Option 2: Run API and web client separately
Terminal 1 - Start the API:
npm run dev:apiTerminal 2 - Start the web client:
npm run dev:clientTerminal 1 - Start the API (if not already running):
npm run dev:apiTerminal 2 - Start the mobile app:
npm run dev:mobileThen:
- Scan the QR code with Expo Go app (iOS/Android)
- Or press
ifor iOS Simulator (Mac only) - Or press
afor Android Emulator
Important for mobile: If testing on a physical device, update the API URL in mobile/src/services/api.js to use your computer's IP address instead of localhost.
See detailed mobile setup instructions in mobile/README.md
- User submits credentials
- Server validates and creates a JWT token
- Token is signed with a secret key and contains user data
- Token is sent back to the client
- Client stores the token in localStorage
- Token is included in subsequent API requests
- Client sends token in Authorization header:
Bearer <token> - Server middleware validates the token
- If valid, request proceeds; if not, returns 401 Unauthorized
- Tokens expire after 1 hour (configurable in
.env) - User must log in again after expiration
-
Token Structure: JWT tokens have three parts separated by dots:
- Header (algorithm & token type)
- Payload (user data & claims)
- Signature (verification)
-
Security Best Practices:
- Never store sensitive data in the token payload (it's base64 encoded, not encrypted)
- Always use HTTPS in production
- Keep the JWT_SECRET secure and never commit it to version control
- Set appropriate token expiration times
-
API Endpoints:
Public Endpoints:
POST /api/auth/register- Register a new userPOST /api/auth/login- Login and receive tokenGET /api/auth/me- Verify token and get current user
Protected Endpoints:
GET /api/protected/dashboard- Requires valid JWT tokenGET /api/protected/profile- Requires valid JWT token
-
Testing with cURL:
# Register curl -X POST http://localhost:5000/api/auth/register \ -H "Content-Type: application/json" \ -d '{"username":"john","email":"john@example.com","password":"password123"}' # Login curl -X POST http://localhost:5000/api/auth/login \ -H "Content-Type: application/json" \ -d '{"email":"john@example.com","password":"password123"}' # Access protected route curl -X GET http://localhost:5000/api/protected/dashboard \ -H "Authorization: Bearer YOUR_TOKEN_HERE"
PORT=5000
JWT_SECRET=your_super_secret_jwt_key_change_this_in_production
JWT_EXPIRES_IN=1hImportant: Change the JWT_SECRET before deploying to production!
- Express.js - Web framework
- jsonwebtoken - JWT token generation and verification
- bcryptjs - Password hashing
- cors - Cross-origin resource sharing
- dotenv - Environment variables
- React - UI library
- Vite - Build tool and dev server
- Fetch API - HTTP requests
- React Native - Mobile framework
- Expo - Development platform
- AsyncStorage - Local data persistence
- Axios - HTTP client
- Start both servers (
npm run dev) - Open http://localhost:3000 in your browser
- Register a new account
- You'll be automatically logged in and see:
- Your user information
- Protected content from the API
- Your JWT token (for educational purposes)
- Try logging out and logging back in
- Check the Network tab in browser DevTools to see the token being sent
- In-memory storage: The API uses an array to store users for simplicity. In production, use a proper database (MongoDB, PostgreSQL, etc.)
- No refresh tokens: This example uses simple access tokens. Production apps should implement refresh tokens
- CORS: The API accepts all origins. Configure this properly for production
- Error handling: Basic error handling is implemented. Expand it for production use
- Password requirements: Add stronger password validation in production
Port already in use: Change ports in api/.env (for API) and client/vite.config.js (for client)
CORS errors: Make sure both servers are running and the proxy is configured correctly
Token invalid: Check that the JWT_SECRET matches between requests and that the token hasn't expired
- Add a database (MongoDB with Mongoose, or PostgreSQL with Sequelize)
- Implement refresh tokens
- Add password reset functionality
- Add email verification
- Implement role-based access control (RBAC)
- Add rate limiting to prevent abuse
MIT - Feel free to use this for educational purposes!