A server application for managing notes with user authentication. It is built with modern backend technologies and follows backend development best practices.
In simple terms: This application works like a digital notepad that can be accessed from anywhere. Each user must log in first, then they can create, read, update, and delete their own notes.
- User Registration & Login — Authentication system with encrypted passwords (bcrypt, configurable cost)
- JWT Token Revocation — Tokens are invalidated server-side on logout; only a SHA-256 hash of the token is stored, never the raw JWT
- Notes CRUD — Create, read, update, and delete notes
- DTO Layer — Explicit request/response types that keep DB schema types internal
- Centralized Error Handling — Custom error classes (
ValidationError,NotFoundError, …) mapped to HTTP statuses by a single middleware - Security Hardening —
helmetsecurity headers, rate limiting on auth endpoints, and request body size limits - Health Checks — Liveness (
GET /) and database-backed readiness (GET /health) endpoints - Graceful Shutdown — Closes the MySQL pool cleanly on
SIGTERM/SIGINT - Database — Secure data storage using MySQL
- Unit Tests — 49 automated tests to ensure code quality
- Hot Reload — Code changes are reflected instantly without restarting the server
| Layer | Technology | Simple Explanation |
|---|---|---|
| Runtime | Bun | A JavaScript/TypeScript runtime. Faster than Node.js |
| Web Framework | Express.js v5 | A library for building web APIs easily |
| ORM | Drizzle ORM | A tool for interacting with the database without writing raw SQL |
| Database | MySQL 8+ | A reliable and widely used database system |
| Authentication | JWT | A token-based system to verify user identity |
| Password Security | bcryptjs | A library for hashing passwords securely |
| Security Headers | helmet | Sets safe HTTP response headers (HSTS, etc.) |
| Rate Limiting | express-rate-limit | Throttles auth endpoints to deter brute-force |
| Request Logging | morgan | Logs each HTTP request for runtime visibility |
📦 belajar-vibe-coding/
├── 📁 src/ # Main application code
│ ├── 📄 app.ts # Express setup (middleware, route mounting)
│ ├── 📄 server.ts # Entrypoint - starts the server
│ │
│ ├── 📁 config/ # Application configuration
│ │ ├── 📄 auth.config.ts # bcrypt cost factor (configurable)
│ │ ├── 📄 cors.config.ts # CORS allowed origins configuration
│ │ ├── 📄 db.config.ts # Database connection configuration (DATABASE_URL required)
│ │ └── 📄 jwt.config.ts # JWT configuration (secret, expiration)
│ │
│ ├── 📁 db/ # Database
│ │ └── 📄 index.ts # Drizzle ORM init, MySQL pool, pingDb/closeDb helpers
│ │
│ ├── 📁 dto/ # Data Transfer Objects
│ │ ├── 📄 auth.dto.ts # Auth request & response types (RegisterRequestDto, LoginResponseDto, …)
│ │ └── 📄 note.dto.ts # Note request & response types (CreateNoteDto, NoteResponseDto, …)
│ │
│ ├── 📁 errors/ # Custom error classes
│ │ └── 📄 app-error.ts # AppError + ValidationError, NotFoundError, ConflictError, …
│ │
│ ├── 📁 schema/ # Database table definitions (Drizzle ORM)
│ │ ├── 📄 auth.schema.ts # 'users' table structure
│ │ ├── 📄 note.schema.ts # 'notes' table structure
│ │ └── 📄 token.schema.ts # 'revoked_tokens' table structure
│ │
│ ├── 📁 repositories/ # Direct database access
│ │ ├── 📄 auth.repository.ts # Queries for users table
│ │ ├── 📄 note.repository.ts # Queries for notes table
│ │ ├── 📄 token.repository.ts # Revoked-token hashing, lookup & cleanup
│ │ └── 📄 db-errors.ts # Detects duplicate-key (unique) DB errors
│ │
│ ├── 📁 services/ # Business logic
│ │ ├── 📄 auth.service.ts # Registration, login, password hashing
│ │ └── 📄 note.service.ts # Notes CRUD logic
│ │
│ ├── 📁 controllers/ # Handle HTTP requests/responses
│ │ ├── 📄 auth.controller.ts # Register, login, logout endpoints
│ │ ├── 📄 health.controller.ts # Liveness & readiness endpoints
│ │ └── 📄 note.controller.ts # Notes CRUD endpoints
│ │
│ ├── 📁 middlewares/ # Functions executed before controllers
│ │ ├── 📄 auth.middleware.ts # JWT token verification & revocation check
│ │ ├── 📄 error.middleware.ts # Centralized error handling + 404 handler
│ │ └── 📄 rate-limit.middleware.ts # Rate limiter for auth endpoints
│ │
│ ├── 📁 routes/ # Endpoint definitions
│ │ ├── 📄 auth.routes.ts # Routes for /api/v1/auth
│ │ ├── 📄 note.routes.ts # Routes for /api/v1/notes
│ │ └── 📄 index.ts # Aggregates all route groups
│ │
│ └── 📁 utils/ # Helper functions
│ ├── 📄 jwt.utils.ts # Generate & verify JWT
│ └── 📄 request-validation.ts # Input validation helpers
│
├── 📁 test/ # All unit tests (49 tests across 23 files)
│ ├── 📁 config/ # Tests for db.config and jwt.config
│ ├── 📁 controllers/ # Tests for auth and note controllers
│ ├── 📁 db/ # Tests for DB initialization
│ ├── 📁 middlewares/ # Tests for auth middleware
│ ├── 📁 repositories/ # Tests for auth and note repositories
│ ├── 📁 routes/ # Tests for route wiring
│ ├── 📁 schema/ # Tests for Drizzle schema definitions
│ ├── 📁 services/ # Tests for auth and note services
│ ├── 📁 utils/ # Tests for JWT utilities
│ └── 📁 test-utils/ # Shared mock helpers
│
├── 📁 drizzle/ # Database migrations
│ ├── 📄 *.sql # Migration files
│ └── 📁 meta/ # Migration metadata
│
├── 📄 package.json # Dependencies & scripts
├── 📄 tsconfig.json # TypeScript configuration
├── 📄 drizzle.config.ts # Drizzle ORM configuration
├── 📄 docker-compose.yml # Docker setup for MySQL
├── 📄 .env.example # Example environment file
└── 📄 README.md # This file
User Request
↓
┌─────────────────────────────────────┐
│ Router (routes/) │ → Determines which endpoint is accessed
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Middleware (middlewares/) │ → Verifies JWT; checks token revocation
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Controller (controllers/) │ → Validates input via request-validation.ts
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ DTO (dto/) │ → Typed request/response shapes crossing layer boundaries
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Service (services/) │ → Business logic, password hashing, token generation
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Repository (repositories/) │ → Sends type-safe queries to the database
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Database (MySQL) │ → Stores/retrieves data
└─────────────────────────────────────┘
↓
JSON Response → User
| Layer | Function | Analogy |
|---|---|---|
| Routes | Defines URL endpoints | Front door |
| Middleware | Verifies JWT and checks token revocation | Security guard |
| Controller | Receives and validates input | Receptionist |
| DTO | Typed shapes passed between controller and service | Official form templates |
| Service | Processes business logic | Chef preparing the dish |
| Repository | Executes database queries | Warehouse clerk |
| Database | Stores and retrieves data | Storage warehouse |
Before running this application, make sure you have installed:
-
Bun >= 1.0
Bun is a modern JavaScript runtime and a faster alternative to Node.js.
Installation instructions are available athttps://bun.sh. -
MySQL 8+
This is used to store application data. You can install it locally or use Docker.
Check your installation:
bun --version
mysql --versiongit clone https://github.com/programmerShinobi/learn-vibe-coding.git
cd learn-vibe-codingWhat does this do?
Cloning downloads the project code from the internet to your computer.
bun installWhat does this do?
This downloads all libraries required by the application, as listed in package.json.
Environment variables store secret configuration values that should not be shared publicly, such as database credentials.
Create a .env file:
cp .env.example .envEdit the .env file with your own values:
# Database configuration (REQUIRED — the app fails to start if unset)
DATABASE_URL="mysql://root:mysql@localhost:3306/vibe_db"
# Format: mysql://username:password@host:port/database_name
# JWT configuration (use a random string with at least 32 characters)
JWT_SECRET="replace-with-a-random-string-of-32-or-more-characters"
# Application port
PORT=3000
# CORS (allowed request origin; empty = allow all, for development)
CORS_ORIGIN=
# bcrypt password-hashing cost factor (OWASP recommends >= 12). Range 10-15.
BCRYPT_COST=12
# Rate limiting for /auth/login and /auth/register
AUTH_RATE_LIMIT_WINDOW_MS=900000 # window length in ms (15 minutes)
AUTH_RATE_LIMIT_MAX=10 # max requests per window per IPNote: Unlike before, there is no baked-in default database URL. Both
DATABASE_URLandJWT_SECRETmust be set or the server refuses to start (fail-fast).BCRYPT_COSTand theAUTH_RATE_LIMIT_*variables are optional and fall back to safe defaults.
Docker lets you run applications inside isolated containers.
docker run -d \
--name mysql-db \
--restart unless-stopped \
-e MYSQL_ROOT_PASSWORD=mysql \
-e MYSQL_DATABASE=vibe_db \
-p 3306:3306 \
mysql:8.0Explanation:
-d= run in the background--name mysql-db= container name--restart unless-stopped= auto-start on boot / Docker restart, so you never have to start it manually again-e MYSQL_ROOT_PASSWORD=mysql= MySQL root password-e MYSQL_DATABASE=vibe_db= create a database namedvibe_db-p 3306:3306= port mappingmysql:8.0= MySQL version used
Check whether MySQL is running:
docker psAlready created the container without a restart policy? If you find yourself running
docker start mysql-dbbefore every session, set the policy once (no data loss, no recreation needed):docker update --restart unless-stopped mysql-dbVerify with:
docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' mysql-db
A docker-compose.yml is included (container name mysql-db, restart: unless-stopped, with a healthcheck). On a fresh machine you can simply run:
docker compose up -dCaution: Compose uses its own named volume. If you already have a
mysql-dbcontainer with data, do not recreate it via Compose — you would start an empty database. Keep your existing container and use thedocker updatecommand above instead.
If MySQL is already installed on your computer:
# Log in to MySQL
mysql -u root -p
# Run this inside the MySQL CLI:
CREATE DATABASE IF NOT EXISTS vibe_db;
EXIT;A migration creates database tables based on the schema you defined in the codebase.
bun run db:pushExpected output:
✓ Database schema pushed successfully
✓ 3 tables created: users, notes, revoked_tokens
bun run devExpected output:
███╗ ██╗ ██████╗ ████████╗███████╗███████╗
████╗ ██║██╔═══██╗╚══██╔══╝██╔════╝██╔════╝
██╔██╗ ██║██║ ██║ ██║ █████╗ ███████╗
██║╚██╗██║██║ ██║ ██║ ██╔══╝ ╚════██║
██║ ╚████║╚██████╔╝ ██║ ███████╗███████║
╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚══════╝╚══════╝
A P I S E R V E R
─────────────────────────────────────────────
▶ Status : Running ✅
▶ Port : http://localhost:3000
▶ Endpoint : http://localhost:3000/api/v1
▶ Mode : development
─────────────────────────────────────────────
The server is now running successfully.
| Command | Function | When to Use |
|---|---|---|
bun run dev |
Start the server with auto-reload | During development |
bun run test |
Run all unit tests (49 tests) | Before pushing changes |
bun run typecheck |
Check for TypeScript errors | Validate syntax without running |
bun run db:push |
Update database schema | After changing schema |
bun run db:generate |
Generate migration files | After updating schema in code |
bun run db:migrate |
Apply migrations to the database | For production setup |
Example:
# Development
bun run dev
# Testing
bun run test
# Database
bun run db:pushThis table stores user information.
| Column | Type | Description |
|---|---|---|
id |
INT | Unique user ID (auto increment) |
name |
VARCHAR(255) | User's name |
email |
VARCHAR(255) | Email address (must be unique) |
password |
VARCHAR(255) | Hashed password |
created_at |
TIMESTAMP | Account creation time |
updated_at |
TIMESTAMP | Last updated time |
Example data:
id | name | email | password (hash) | created_at
1 | John Doe | john@example.com | $2a$12$xxxx... | 2024-06-03
This table stores all notes created by users.
| Column | Type | Description |
|---|---|---|
id |
INT | Unique note ID |
user_id |
INT | ID of the user who created the note (FK) |
title |
VARCHAR(255) | Note title |
content |
TEXT | Note content |
created_at |
TIMESTAMP | Note creation time |
updated_at |
TIMESTAMP | Last updated time |
Example data:
id | user_id | title | content | created_at
1 | 1 | My Notes | This is a note... | 2024-06-03
2 | 1 | To Do List | 1. Buy milk... | 2024-06-03
Table relationship:
users (1) ──── (Many) notes
via user_id
Stores JWTs that have been explicitly invalidated on logout. The auth middleware checks this table on every request to reject revoked tokens before their natural expiry.
| Column | Type | Description |
|---|---|---|
id |
INT | Primary key (auto increment) |
token_hash |
VARCHAR(64), UNIQUE | SHA-256 hash of the revoked JWT (the raw token is never stored) |
expires_at |
TIMESTAMP | Token's original expiry time (used for cleanup) |
created_at |
TIMESTAMP | When the token was revoked |
Security: Only a SHA-256 hash of the token is persisted, so a database leak does not expose usable bearer tokens. Lookups hash the incoming token and compare. Expired rows are purged automatically by a periodic cleanup job (hourly), so the table does not grow without bound.
http://localhost:3000/api/v1
Example endpoints:
http://localhost:3000/api/v1/auth/registerhttp://localhost:3000/api/v1/auth/loginhttp://localhost:3000/api/v1/notes
All request bodies use form-data:
Content-Type: application/x-www-form-urlencoded
Example:
name=John Doe&email=john@example.com&password=secret123
All responses are JSON with the same structure:
{
"message": "Descriptive message",
"data": {}
}The data field can be an object, an array, or null.
Success example:
{
"message": "User logged in successfully",
"data": {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"token": "eyJhbGciOiJIUzI1NiIs..."
}
}Error example:
{
"message": "Validation error",
"data": null
}These endpoints are unauthenticated and useful for monitoring / orchestrators.
GET /Confirms the process is up and serving.
{ "message": "Notes API is running", "data": null }GET /healthConfirms the app can serve traffic by pinging the database. Returns 200 when the database is reachable and 503 when it is not.
{ "message": "ok", "data": { "database": "up" } }POST /api/v1/auth/registerCreates a new account using a name, email, and password.
Required fields:
| Field | Type | Required | Example |
|---|---|---|---|
name |
string | Yes | John Doe |
email |
string | Yes | john@example.com |
password |
string | Yes | secret123 |
Example using curl:
curl -X POST http://localhost:3000/api/v1/auth/register \
-d "name=John Doe&email=john@example.com&password=password123"Successful response (201 Created):
{
"message": "User created successfully",
"data": {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"createdAt": "2024-06-03T10:00:00.000Z",
"updatedAt": "2024-06-03T10:00:00.000Z"
}
}Error response (409 - email already registered):
{
"message": "User already exists",
"data": null
}Registration relies on the database's unique constraint on
409 Conflict.
POST /api/v1/auth/loginLogs in a user and returns an authentication token.
Required fields:
| Field | Type | Required |
|---|---|---|
email |
string | Yes |
password |
string | Yes |
Example using curl:
curl -X POST http://localhost:3000/api/v1/auth/login \
-d "email=john@example.com&password=password123"Successful response (200 OK):
{
"message": "User logged in successfully",
"data": {
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"createdAt": "2024-06-03T10:00:00.000Z",
"updatedAt": "2024-06-03T10:00:00.000Z",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}Important: Save this token. It is valid for 1 day and is required to access notes endpoints.
POST /api/v1/auth/logout
Authorization: Bearer <token>Logs the user out and revokes the token so it can no longer be used.
Header:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Example using curl:
curl -X POST http://localhost:3000/api/v1/auth/logout \
-H "Authorization: Bearer <token>"Successful response (200 OK):
{
"message": "User logged out successfully",
"data": null
}Important: All notes endpoints require the following header:
Authorization: Bearer <token>
Replace <token> with the token you received during login.
POST /api/v1/notes
Authorization: Bearer <token>Required fields:
| Field | Type | Required |
|---|---|---|
title |
string | Yes |
content |
string | Yes |
Example using curl:
curl -X POST http://localhost:3000/api/v1/notes \
-H "Authorization: Bearer <token>" \
-d "title=Important Note&content=Do not forget to buy milk"Successful response (201 Created):
{
"message": "Note created successfully",
"data": {
"id": 1,
"userId": 1,
"title": "Important Note",
"content": "Do not forget to buy milk",
"createdAt": "2024-06-03T10:00:00.000Z",
"updatedAt": "2024-06-03T10:00:00.000Z"
}
}GET /api/v1/notes
Authorization: Bearer <token>Returns all notes created by the currently logged-in user.
Example using curl:
curl -X GET http://localhost:3000/api/v1/notes \
-H "Authorization: Bearer <token>"Successful response (200 OK):
{
"message": "Notes retrieved successfully",
"data": [
{
"id": 1,
"userId": 1,
"title": "Important Note",
"content": "Do not forget to buy milk",
"createdAt": "2024-06-03T10:00:00.000Z",
"updatedAt": "2024-06-03T10:00:00.000Z"
},
{
"id": 2,
"userId": 1,
"title": "To Do List",
"content": "1. Buy milk\n2. Study coding",
"createdAt": "2024-06-03T10:05:00.000Z",
"updatedAt": "2024-06-03T10:05:00.000Z"
}
]
}GET /api/v1/notes/:id
Authorization: Bearer <token>Parameter:
id= note ID
Example using curl:
curl -X GET http://localhost:3000/api/v1/notes/1 \
-H "Authorization: Bearer <token>"Successful response (200 OK):
{
"message": "Note retrieved successfully",
"data": {
"id": 1,
"userId": 1,
"title": "Important Note",
"content": "Do not forget to buy milk",
"createdAt": "2024-06-03T10:00:00.000Z",
"updatedAt": "2024-06-03T10:00:00.000Z"
}
}Error response (404 Not Found):
{
"message": "Note not found",
"data": null
}GET /api/v1/notes/user/:userId
Authorization: Bearer <token>Parameter:
userId= ID of the user whose notes you want to retrieve
Example using curl:
curl -X GET http://localhost:3000/api/v1/notes/user/1 \
-H "Authorization: Bearer <token>"Successful response (200 OK):
{
"message": "Notes retrieved successfully",
"data": [
{
"id": 1,
"userId": 1,
"title": "Important Note",
"content": "...",
"createdAt": "...",
"updatedAt": "..."
}
]
}PUT /api/v1/notes/:id
Authorization: Bearer <token>Parameter:
id= note ID to update
Request fields: at least one of the following must be provided.
| Field | Type | Required |
|---|---|---|
title |
string | One of the two |
content |
string | One of the two |
Example using curl:
curl -X PUT http://localhost:3000/api/v1/notes/1 \
-H "Authorization: Bearer <token>" \
-d "title=Updated Note&content=New content"Successful response (200 OK):
{
"message": "Note updated successfully",
"data": {
"id": 1,
"userId": 1,
"title": "Updated Note",
"content": "New content",
"createdAt": "2024-06-03T10:00:00.000Z",
"updatedAt": "2024-06-03T10:10:00.000Z"
}
}DELETE /api/v1/notes/:id
Authorization: Bearer <token>Parameter:
id= note ID to delete
Example using curl:
curl -X DELETE http://localhost:3000/api/v1/notes/1 \
-H "Authorization: Bearer <token>"Successful response (200 OK):
{
"message": "Note deleted successfully",
"data": null
}| HTTP Code | Meaning | Cause | Solution |
|---|---|---|---|
| 200 | OK | Request succeeded | — |
| 201 | Created | Resource created successfully | — |
| 400 | Bad Request | Invalid or incomplete input | Check the submitted data |
| 401 | Unauthorized | Missing, invalid, or expired token | Log in again and get a new token |
| 403 | Forbidden | Authenticated but not allowed (e.g. another user's notes) | Use your own resources |
| 404 | Not Found | Requested resource does not exist | Check the ID or endpoint URL |
| 409 | Conflict | Resource already exists (e.g. duplicate email) | Use a different value |
| 429 | Too Many Requests | Rate limit hit on an auth endpoint | Wait for the window to reset, then retry |
| 500 | Server Error | Internal server issue | Contact the developer |
| 503 | Service Unavailable | A dependency (the database) is unreachable | Check that MySQL is running |
All errors are produced by a single centralized error-handling middleware, so every error response shares the same { "message": ..., "data": null } shape.
Example error responses:
400 - Validation failed
{
"message": "Email is required",
"data": null
}401 - Invalid token
{
"message": "Unauthorized: Token missing or invalid",
"data": null
}404 - Note not found
{
"message": "Note not found",
"data": null
}This application includes 49 unit tests across 23 files to ensure code quality.
Run all tests:
bun run testExpected output:
49 pass
0 fail
Ran 49 tests across 23 files.
Test coverage includes:
| Area | What is tested |
|---|---|
| Config | DB and JWT configuration loading & validation |
| DB | Drizzle ORM initialization |
| Schema | Drizzle table definitions |
| Repositories | Auth/note queries, revoked-token hashing, duplicate-key detection |
| Services | Business logic for auth and notes |
| Controllers | Register, login, logout, notes CRUD, and health handlers |
| Middleware | JWT verification, token revocation, and centralized error handling |
| Routes | Endpoint wiring for /auth and /notes |
| Utils | JWT generate and verify helpers |
| Test utils | Shared mock helpers |
Each layer of the application is tested to help prevent bugs.
JWT (JSON Web Token) is a secure way to identify users without sending their password on every request.
Simple analogy: It is like a movie ticket. After you buy the ticket, you do not need to pay again each time you enter. You just show the ticket.
How it works:
- The user logs in with email and password
- The server generates a token and sends it to the user
- The user stores the token and includes it in every request
- The server verifies the token; if valid, access is granted
Token validity: 1 day
bcryptjs is a library used to hash passwords. This means passwords are stored securely and cannot be read in plain text, even by the server administrator.
Example:
Original password: "password123"
Hashed password: "$2a$12$kN21cpF3/Us.MNNC3z0CuO2/q94F.."
Passwords are never stored in their original form. Only the hash is saved. The hashing cost factor defaults to 12 (per OWASP guidance) and is configurable via the BCRYPT_COST environment variable.
An ORM (Object-Relational Mapping) acts like a translator between JavaScript code and an SQL database.
Without ORM (raw SQL):
SELECT * FROM users WHERE id = 1;With ORM (Drizzle):
const user = await db.select().from(users).where(eq(users.id, 1));Using an ORM makes code easier to read and helps protect against SQL injection.
This project follows the Clean Architecture pattern:
routes/ - Entry point (URL routing)
↓
controllers/ - Receives & validates HTTP requests
↓
dto/ - Typed request/response shapes (no DB types leak across layers)
↓
services/ - Business logic
↓
repositories/ - Database queries (Drizzle schema types stay here)
↓
database/ - Actual data storage
Each layer has a clear and separate responsibility. This makes the codebase easier to maintain and test.
Solution:
bun installMeaning: MySQL is not running. (GET /health will also return 503 in this case.)
Solution:
- If using Docker:
docker start mysql-db - To stop having to start it manually every session, set a restart policy once:
docker update --restart unless-stopped mysql-db - If using local MySQL: make sure the MySQL service is running
Meaning: Your database schema is older than the code (the revoked_tokens table still has the legacy token column).
Solution:
- Sync the schema:
bun run db:push - This project's revoked-token storage moved from the raw
tokencolumn to a hashedtoken_hashcolumn (migration0003).
Meaning: A required environment variable is missing — the app fails fast instead of using insecure defaults.
Solution:
- Ensure your
.envfile exists (cp .env.example .env) and both values are set.
Meaning: The token is expired or incorrect.
Solution:
- Log in again to get a new token
- Tokens are only valid for 1 day
Meaning: The email is already registered.
Solution:
- Use a different email address when registering
- Express.js Documentation
- Drizzle ORM Documentation
- JWT.io
- Bun Official Documentation
- MySQL Documentation
If you want to contribute or report a bug:
- Fork the repository
- Create a new branch:
git checkout -b feature/your-feature-name - Commit your changes:
git commit -am 'Add feature' - Push the branch:
git push origin feature/your-feature-name - Open a Pull Request
MIT — free to use for both commercial and personal purposes.
For questions or suggestions, please open an issue in the GitHub repository.