A production-oriented, reusable authentication and user-management REST API built with Express, PostgreSQL, Prisma, Passport, JWT, Zod, and secure HTTP-only cookies.
This project is designed to be used as a starting point for future web applications. It provides the authentication and account-management foundation so application-specific features can be built on top of it without repeatedly rebuilding authentication infrastructure.
The template is intentionally modular: authentication, sessions, OAuth, validation, email workflows, and user management are separated into focused modules that can be reused or extended as needed.
-
Local email/password authentication
-
Google OAuth 2.0
-
GitHub OAuth
-
JWT access tokens
-
JWT refresh tokens
-
HTTP-only authentication cookies
-
Refresh-token rotation
-
Server-side session tracking
-
Session revocation
-
Authentication middleware
-
Secure logout
-
Password changes
-
Password reset flow
-
Email verification
-
Email address change verification
-
Account deletion
-
Password hashing with
bcryptjs -
Separate access-token and refresh-token secrets
-
Refresh-token hashes stored in the database instead of raw tokens
-
Refresh-token rotation and reuse detection
-
Session revocation after password changes and password resets
-
Cryptographically secure verification codes and tokens
-
OAuth state validation using
crypto.timingSafeEqual -
Rate limiting on authentication-sensitive endpoints
-
Zod request validation
-
Helmet security headers
-
Credentialed CORS restricted to the configured frontend origin
-
Centralized error handling
-
Prisma database constraints as the final protection against race conditions
-
Generic authentication errors that avoid unnecessarily exposing account information
-
Secure cookie configuration for production environments
-
ES modules
-
Prisma ORM
-
PostgreSQL
-
Vitest
-
Supertest
-
ESLint
-
Prettier
-
Environment validation with Zod
-
Modular service/controller architecture
-
Reusable authentication foundation
-
Comprehensive automated test suite
Optional email infrastructure
Authentication is infrastructure.
It is something almost every full-stack application needs, but it is also an area where small implementation mistakes can create serious security problems.
Instead of rebuilding registration, login, sessions, password resets, email verification, OAuth, rate limiting, and account management for every application, this project provides a reusable foundation.
The intended workflow is:
Express Auth API
│
┌──────────────┼──────────────┐
│ │ │
Authentication User Accounts Sessions
│ │ │
└──────────────┼──────────────┘
│
Your Application
│
┌──────────────┼──────────────┐
│ │ │
Posts Messages Projects
Application-specific functionality should be built on top of the authentication layer rather than tightly coupling business logic to it.
For example, a future application could add:
src/
├── controllers/
│ ├── auth.controller.js
│ ├── oauth.controller.js
│ └── user.controller.js
│
├── services/
│ ├── auth.service.js
│ ├── session.service.js
│ ├── user.service.js
│ └── ...
│
└── ...
and then introduce its own application-specific modules without having to redesign authentication.
The API follows a layered architecture:
HTTP Request
│
▼
Routes
│
▼
Middleware
├── Rate limiting
├── Authentication
├── Validation
└── Passport
│
▼
Controllers
│
▼
Services
│
▼
Prisma
│
▼
PostgreSQL
| Layer | Responsibility |
| -------------- | --------------------------------------------------------------- |
| routes/ | Defines HTTP endpoints and middleware composition |
| middleware/ | Authentication, validation, rate limiting, error handling |
| controllers/ | Handles HTTP requests and responses |
| services/ | Contains application and authentication business logic |
| strategies/ | Passport authentication strategies and OAuth profile processing |
| schemas/ | Zod request validation schemas |
| config/ | Environment, cookies, and Passport configuration |
| db/ | Prisma client/database configuration |
| emails/ | Email content/templates |
| errors/ | Application-specific error types |
| utils/ | Small reusable utility functions |
The goal is to keep HTTP concerns, authentication logic, persistence, and reusable utilities from becoming unnecessarily intertwined.
The local login flow is:
POST /auth/login
│
▼
Request validation
│
▼
Rate limiter
│
▼
Passport Local Strategy
│
├── Find user
├── Verify password
└── Verify email
│
▼
Create session
│
├── Generate access token
└── Generate refresh token
│
▼
HTTP-only cookies
The API does not return authentication tokens in the JSON response.
Instead, tokens are stored in HTTP-only cookies.
Access tokens are short-lived JWTs containing the authenticated user's identifier.
They are used to authenticate normal API requests.
Client
│
│ accessToken cookie
▼
authenticate middleware
│
├── Verify JWT
├── Find user
└── Attach user to req.user
Refresh tokens are longer-lived JWTs associated with a server-side session.
The database stores a SHA-256 hash of the refresh token rather than the raw token.
A refresh request:
Refresh Token
│
▼
Verify JWT
│
▼
Find Session
│
▼
Compare Token Hash
│
▼
Rotate Token
│
├── Replace stored hash
├── Update lastUsedAt
└── Extend expiration
│
▼
Issue New Access + Refresh Tokens
If a previously rotated refresh token is reused, the session is revoked.
This gives the application server-side control over otherwise stateless JWT refresh credentials.
Google and GitHub authentication are implemented through Passport.
The flow is intentionally separated into multiple stages:
OAuth Provider
│
▼
Passport Strategy
│
▼
Provider Profile Processor
│
▼
OAuth Service
│
├── Find existing account
├── Find existing user
├── Generate username
└── Create user + account
│
▼
Create Authentication Session
│
▼
HTTP-only Cookies
Provider-specific profile processing is kept separate from database operations so additional OAuth providers can be added without placing provider-specific logic inside the core OAuth service.
Google and GitHub are optional. The application only configures a provider when its required environment variables are present.
server/
├── generated/
│ └── prisma/
│
├── prisma/
│ └── schema.prisma
│
├── src/
│ ├── config/
│ │ ├── cookies.js
│ │ ├── env.js
│ │ └── passport.js
│ │
│ ├── controllers/
│ │ ├── auth.controller.js
│ │ ├── oauth.controller.js
│ │ └── user.controller.js
│ │
│ ├── db/
│ │ └── prisma.js
│ │
│ ├── emails/
│ │ ├── email-change.js
│ │ ├── email-verification.js
│ │ └── password-reset.js
│ │
│ ├── errors/
│ │ └── AppError.js
│ │
│ ├── middleware/
│ │ ├── authenticate.js
│ │ ├── error-handler.js
│ │ ├── passport.js
│ │ ├── rate-limit.js
│ │ └── validate.js
│ │
│ ├── routes/
│ │ ├── auth.routes.js
│ │ └── user.routes.js
│ │
│ ├── schemas/
│ │ ├── auth.schema.js
│ │ ├── common.schema.js
│ │ └── user.schema.js
│ │
│ ├── services/
│ │ ├── auth.service.js
│ │ ├── email-change.service.js
│ │ ├── email-verification.service.js
│ │ ├── email.service.js
│ │ ├── oauth.service.js
│ │ ├── oauth.state.service.js
│ │ ├── password-reset.service.js
│ │ ├── password.service.js
│ │ ├── session.service.js
│ │ ├── token.service.js
│ │ ├── user.service.js
│ │ └── verification-token.service.js
│ │
│ ├── strategies/
│ │ ├── github-profile.js
│ │ ├── github.strategy.js
│ │ ├── google-profile.js
│ │ ├── google.strategy.js
│ │ └── local.strategy.js
│ │
│ ├── utils/
│ │ └── duration.js
│ │
│ ├── app.js
│ └── server.js
│
├── tests/
│ ├── auth/
│ ├── strategies/
│ ├── users/
│ └── setup.js
│
├── .env.example
├── .gitignore
├── package.json
└── README.md
Before starting, make sure you have:
-
Node.js
-
npm
-
PostgreSQL
-
A PostgreSQL database for the application
-
A PostgreSQL database for tests
Optional:
-
Google OAuth credentials
-
GitHub OAuth credentials
-
Resend account/API key
Clone the repository:
git clone https://github.com/JavedanCode/express-auth-api-template.git
Enter the project:
cd express-auth-api-template
Install dependencies:
npm install
Create your local environment file from the provided example:
.env.example
Copy it to:
.env
Then configure the required values.
The repository intentionally does not include real credentials.
NODE_ENV=development
PORT=3000
DATABASE_URL="postgresql://USERNAME:PASSWORD@HOST:5432/DATABASE_NAME"
CLIENT_URL="http://localhost:5173"
JWT_ACCESS_SECRET="your-access-token-secret"
JWT_REFRESH_SECRET="your-refresh-token-secret"
RESEND_API_KEY="your-resend-api-key"
EMAIL_FROM="your-sender@example.com"
PASSWORD_RESET_URL="http://localhost:5173/reset-password"
EMAIL_CHANGE_URL="http://localhost:5173/change-email"
JWT secrets should be long, unpredictable values and should be different from one another.
Google and GitHub authentication are optional.
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
GOOGLE_CALLBACK_URL="http://localhost:3000/auth/google/callback"
GITHUB_CLIENT_ID=""
GITHUB_CLIENT_SECRET=""
GITHUB_CALLBACK_URL="http://localhost:3000/auth/github/callback"
If a provider is not configured, its Passport strategy is simply not registered.
The project uses PostgreSQL through Prisma.
After configuring DATABASE_URL, run:
npx prisma migrate dev
Generate the Prisma client if necessary:
npx prisma generate
The database schema is located at:
prisma/schema.prisma
For production deployments, use Prisma migrations rather than manually modifying the production database schema.
Start the development server:
npm run dev
Start the application normally:
npm start
The API will be available at:
http://localhost:3000
The health endpoint can be used to verify that the API is running:
GET /health
Example response:
{
"success": true,
"message": "API is running."
}The project includes integration and unit tests using Vitest and Supertest.
Tests use a separate environment file:
.env.test
Make sure the test database is configured before running the test suite.
Run all tests:
npm test
Run a specific test file:
npm test -- tests/auth/login.test.js
The test suite covers authentication, sessions, OAuth profile processing, email verification, password reset, account management, validation, rate limiting, and other authentication behavior.
Run ESLint:
npm run lint
Format the project:
npm run format
Check formatting without modifying files:
npm run format:check
Before opening a pull request or using the template as the foundation for another project, it is recommended to run:
npm test
npm run lint
npm run format:check
All authentication and account-management endpoints are grouped into two primary route namespaces:
/auth
/users
Authentication state is primarily maintained through secure HTTP-only cookies.
Creates a new local user account.
{
"username": "johndoe",
"email": "john@example.com",
"password": "StrongPassword123!"
}Registration behavior depends on the EMAIL_ENABLED configuration.
When email functionality is enabled, registration creates an unverified account and sends an email verification message.
When email functionality is disabled, the account is automatically marked as verified and no verification email or verification token is created.
{
"success": true,
"message": "Registration successful. Please verify your email address.",
"user": {
"id": "...",
"username": "johndoe",
"email": "john@example.com",
"displayName": null,
"avatarUrl": null,
"emailVerifiedAt": null
}
}Authenticates a verified local user.
{
"email": "john@example.com",
"password": "StrongPassword123!"
}Successful authentication sets:
-
accessToken -
refreshToken
as HTTP-only cookies.
Authentication tokens are not returned in the JSON response.
Logs the current session out and clears authentication cookies.
The endpoint is intentionally safe to call even when the refresh token is missing, invalid, or expired.
Rotates the current refresh token and issues a new access token.
The refresh token must be supplied through the authentication cookie.
Returns the currently authenticated user.
Requires authentication.
Verifies a user's email address.
{
"email": "john@example.com",
"code": "123456"
}Verification codes are:
-
Cryptographically generated
-
Hashed before database storage
-
Short-lived
-
Single-use
-
Protected by resend cooldowns
Requests another verification email.
The endpoint intentionally returns a generic success response so that it does not unnecessarily reveal whether a specific email belongs to an account.
Requests a password reset email.
{
"email": "john@example.com"
}The endpoint intentionally uses a generic response regardless of whether the account exists.
If email functionality is disabled, the endpoint returns:
{ "success": false, "error": { "code": "EMAIL_FEATURE_DISABLED", "message": "Password reset is unavailable because email functionality is disabled." } }
Resets a password using a valid reset token.
{
"token": "reset-token",
"newPassword": "NewStrongPassword123!"
}Resetting a password also revokes the user's active sessions.
All user-management endpoints require authentication.
Updates the user's profile.
{
"displayName": "John Doe",
"avatarUrl": "https://example.com/avatar.jpg"
}At least one profile field must be supplied.
Changes the authenticated user's password.
{
"currentPassword": "CurrentPassword123!",
"newPassword": "NewPassword123!"
}Changing the password revokes all active sessions.
Changes the username.
{
"username": "newusername"
}Requests an email address change.
{
"email": "new@example.com"
}The new address must be confirmed through the verification email before the account's email address is changed.
If email functionality is disabled, this endpoint returns:
{ "success": false, "error": { "code": "EMAIL_FEATURE_DISABLED", "message": "Email change is unavailable because email functionality is disabled." } }
Confirms an email address change.
{
"token": "email-change-token"
}Deletes the authenticated user's account.
Local-password accounts must provide the current password.
{
"currentPassword": "CurrentPassword123!"
}OAuth-only accounts do not have a local password and therefore do not require password confirmation.
Start authentication:
GET /auth/google
Google authorization:
GET /auth/google/authorize
Google callback:
GET /auth/google/callback
Start authentication:
GET /auth/github
GitHub authorization:
GET /auth/github/authorize
GitHub callback:
GET /auth/github/callback
OAuth authentication uses a cryptographically random state value stored in an HTTP-only cookie and validated during the callback.
The API uses a consistent error response structure.
Example:
{
"success": false,
"error": {
"code": "EMAIL_ALREADY_EXISTS",
"message": "Email is already registered."
}
}Validation errors additionally include field-level details:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed.",
"details": [
{
"field": "password",
"message": "Password must be at least 8 characters long."
}
]
}
}Application-specific errors use AppError.
Database errors that represent expected conditions, such as unique-constraint violations, are translated into appropriate API responses by the centralized error handler.
Unexpected errors are intentionally exposed as a generic:
{
"success": false,
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred."
}
}Request bodies are validated using Zod before reaching controllers.
The validation flow is:
Request
│
▼
Zod Schema
│
├── Invalid → 400 Validation Error
│
└── Valid
│
▼
req.body
│
▼
Controller
Validation schemas are kept separate from business logic.
Common validation rules are shared through:
src/schemas/common.schema.js
This prevents rules such as password requirements from being duplicated across authentication endpoints.
Authentication-sensitive endpoints use dedicated rate limiters.
Current protected operations include:
| Operation | Window | Limit |
| ------------------------- | ---------: | ----: |
| Login | 15 minutes | 10 |
| Registration | 1 hour | 5 |
| Refresh | 15 minutes | 20 |
| Email verification | 15 minutes | 10 |
| Verification email resend | 1 hour | 5 |
| Password reset request | 15 minutes | 5 |
| Password reset | 15 minutes | 5 |
These limits are intended as sensible defaults for the template and should be reviewed according to the requirements and threat model of the application where the template is deployed.
This template is designed with several security boundaries in mind.
Passwords are never stored in plaintext.
They are hashed using bcryptjs before being persisted.
Plaintext password
│
▼
bcrypt
│
▼
Password hash
│
▼
PostgreSQL
Raw refresh tokens are not stored in the database.
Instead:
Refresh Token
│
▼
SHA-256
│
▼
Token Hash
│
▼
PostgreSQL
This means database access alone does not expose usable refresh credentials.
Every successful refresh operation replaces the stored refresh-token hash.
A previously used refresh token therefore cannot be used again.
If token reuse is detected, the associated session is revoked.
Sessions can be revoked individually or for an entire user.
All active sessions are revoked when:
-
A user changes their password
-
A user resets their password
-
A refresh-token reuse attempt is detected
This provides server-side invalidation even though authentication uses JWTs.
Authentication cookies are configured with:
-
httpOnly -
sameSite -
securein production -
Restricted paths
-
Explicit expiration
The access token and refresh token use separate cookie configurations.
OAuth flows generate a cryptographically random state value.
The callback verifies the received state using a timing-safe comparison before accepting the authentication result.
This helps protect the OAuth callback from forged or unsolicited authorization responses.
Verification and reset credentials are not stored in plaintext.
The system uses:
-
Cryptographically secure random values
-
SHA-256 hashes
-
Expiration times
-
Single-use tokens
-
Request cooldowns
Password-reset and email-change flows also invalidate previously active tokens where appropriate.
The API allows credentialed cross-origin requests only from the configured frontend origin:
CLIENT_URL="http://localhost:5173"
The frontend must therefore be explicitly configured as the allowed origin.
For production deployments, CLIENT_URL should point to the actual frontend origin rather than using a wildcard.
Email delivery is implemented through Resend.
The email service is intentionally isolated behind:
src/services/email.service.js
Application services do not need to know how email is delivered.
They simply call:
await sendEmail({
to,
subject,
html,
});This keeps email infrastructure replaceable if a future application needs a different provider.
The authentication layer is intended to remain independent from application-specific functionality.
For example, if this template is used for a social application, application-specific functionality could be organized separately:
src/
├── controllers/
│ ├── auth.controller.js
│ ├── oauth.controller.js
│ ├── user.controller.js
│ ├── post.controller.js
│ └── comment.controller.js
│
├── routes/
│ ├── auth.routes.js
│ ├── user.routes.js
│ ├── post.routes.js
│ └── comment.routes.js
│
└── services/
├── auth.service.js
├── user.service.js
├── session.service.js
├── post.service.js
└── comment.service.js
The authentication system should not need to know what the application does with authenticated users.
Instead, application-specific routes can simply use:
authenticate;to establish the authenticated user context.
When starting a new project from this repository, the recommended process is:
git clone https://github.com/JavedanCode/express-auth-api-template.git my-new-project
Create a new Git repository for the actual application rather than continuing development directly on the template repository.
Create .env from .env.example.
Create a project-specific database and update DATABASE_URL.
Set:
CLIENT_URL="..."
Decide whether the application requires email functionality.
If email is not required, leave:
EMAIL_ENABLED=false
If email functionality is required, set:
EMAIL_ENABLED=true
and configure the Resend API key, sender address, and email URLs.
Add Google and/or GitHub credentials if the application needs social authentication.
Add application-specific models while preserving the authentication models and relationships required by the application.
Keep new business logic separate from the authentication infrastructure.
This template provides the application-level foundation for production-oriented authentication, but deploying a real application still requires environment and infrastructure configuration appropriate for the deployment.
Before deploying:
-
Use HTTPS/TLS
-
Set
NODE_ENV=production -
Use strong, unique JWT secrets
-
Never commit
.env -
Use production PostgreSQL credentials
-
Configure the correct frontend origin
-
Configure OAuth callback URLs for the production domain
-
Configure a production email sender if email functionality is enabled
-
Review rate limits for the application's traffic and threat model
-
Keep Node.js and dependencies up to date
-
Run database migrations as part of the deployment process
-
Use appropriate process management and infrastructure for the hosting environment
-
Monitor application errors and authentication activity
-
Review the application's CORS, cookie, and proxy configuration
The template is deliberately not tied to a specific hosting provider.
This project follows a few principles:
Authentication should not depend on whether the application is a blog, social network, messaging application, dashboard, marketplace, or something else.
Controllers handle HTTP.
Services handle business logic.
Prisma handles persistence.
Middleware handles cross-cutting request concerns.
Incoming data is validated before it reaches application logic.
Application-level checks provide useful errors, but database constraints remain the final authority for uniqueness and relational integrity.
Authentication behavior that is security-sensitive should be easy to locate and understand.
The template is modular, but it intentionally avoids introducing abstractions that do not provide meaningful value.
Contributions are welcome.
If you find a bug, security issue, documentation problem, or improvement that would make the template more useful to other developers, feel free to open an issue or submit a pull request.
When contributing:
-
Keep changes focused.
-
Preserve the existing architecture unless there is a strong reason to change it.
-
Add or update tests for behavioral changes.
-
Run the test suite.
-
Run ESLint.
-
Run the formatter.
-
Avoid introducing application-specific functionality into the authentication core.
-
Document security-sensitive architectural changes.
For significant architectural changes, open an issue first so the proposed approach can be discussed before implementation.
Please do not publicly disclose a potentially exploitable security vulnerability in an issue before giving the maintainer an opportunity to investigate it.
For serious security issues, contact the repository maintainer privately through the contact information available on the maintainer's GitHub profile.
This project is licensed under the MIT License.
You are free to:
-
Use the template in personal projects
-
Use the template in commercial projects
-
Modify the source code
-
Distribute modified versions
-
Build proprietary applications using the template
See the LICENSE file for the complete license text.
This project is built on the following open-source technologies:
-
Express
-
Prisma
-
PostgreSQL
-
Passport
-
JSON Web Tokens
-
Zod
-
bcryptjs
-
Helmet
-
express-rate-limit
-
Resend
-
Vitest
-
Supertest
This repository is intended to serve as a reusable authentication API foundation rather than a finished end-user application.
The authentication and user-management functionality is implemented and covered by automated tests. The template can be extended with application-specific functionality as required.
JavedanCode
Built as a reusable foundation for future full-stack applications and client projects.
If you find the project useful, feel free to fork it, adapt it, and build something great with it.