yntk-ts
You Need This Kit - Type-safe Starter: REST API boilerplate that ships with User Management, Role Management, and Authentication with JWTs, Prisma ORM, and PostgreSQL
You Need This Kit - Type-safe Starter: REST API boilerplate that ships with User Management, Role Management, and Authentication with JWTs, Prisma ORM, and PostgreSQL
You Need This Kit - Type-safe Starter: REST API boilerplate that ships with User Management, Role Management, and Authentication with JWTs, Prisma ORM, and PostgreSQL
TypeScript/Express REST API starter kit that ships with User Management, Role Management, and Authentication with JWTs, Prisma ORM, and PostgreSQL. The project is organized by feature with explicit service/repository layers so business logic stays separated from transport concerns.
src/
├── app.ts # Express app bootstrap
├── server.ts # Starts HTTP server
├── config/ # Environment loader & Prisma client wrapper
├── controllers/ # HTTP handlers grouped by module + shared helpers
├── services/ # Business logic (auth/user) & mappers
├── repositories/ # Prisma data access per module
├── middleware/ # Cross-cutting middleware (auth, validation)
├── routes/ # Express routers mounted under /auth and /users
├── validations/ # Zod schemas for request validation
├── views/ # Email templates
├── errors/ # Custom AppError type & error utilities
├── utils/ # Shared utilities (password, string, Zod helpers)
└── types/ # Shared TS types & Express module augmentation
pnpm install
Create .env (never commit it) with the required settings:
# Server Configuration
NODE_ENV=development
FRONTEND_URL=http://localhost:8080
PORT=5050
LOG_LEVEL=info
# Database
DATABASE_URL=postgresql://USER:PASSWORD@HOST:PORT/DATABASE
# JWT Configuration
JWT_SECRET=super-secret
# Bcrypt Configuration
SALT_ROUNDS=10
# Email Configuration
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_USER=your-email@gmail.com
EMAIL_PASSWORD=your-app-password
EMAIL_FROM=noreply@yourapp.com
# Token Configuration
ACCESS_TOKEN_EXPIRY="1h"
ENABLE_REFRESH_TOKEN=true
REFRESH_TOKEN_IN_JSON=true
REFRESH_TOKEN_IN_COOKIE=true
REFRESH_TOKEN_EXPIRY=7 * 24 * 60 * 60 * 1000 # 7 days in miliseconds
COOKIE_SAME_SITE="strict" # strict, lax, none
RESET_PASSWORD_TOKEN_EXPIRY=1 * 60 * 60 * 1000 # 1 hour in miliseconds
EMAIL_VERIFICATION_TOKEN_EXPIRY=24 * 60 * 60 * 10000 # 24 hours in miliseconds
# CORS Configuration
ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,http://localhost:8080,https://yourdomain.com
CORS_CREDENTIALS=true
Note for Gmail: Use an App Password instead of your regular password. Enable 2FA and generate an App Password in Google Account Settings → Security → App passwords.
prisma/schema.prisma.pnpm prisma migrate dev (for local) or pnpm prisma db push for quick sync.pnpm prisma generate. Output lands in src/generated/prisma.pnpm prisma db seedpnpm dev
Runs tsx in watch mode, recompiling on changes. The API listens on PORT from the env file (defaults to 5050).
| Command | Purpose |
|---|---|
pnpm dev |
Start the API in watch mode with tsx |
pnpm prisma migrate dev |
Create/apply migrations and regenerate Prisma client |
pnpm prisma generate |
Regenerate Prisma client manually |
pnpm prisma db push |
Quick sync schema to database without migrations |
pnpm prisma db seed |
Seed the database |
pnpm build |
Build the API for production (bundles with tsup) |
pnpm start |
Start the production server from dist/ |
pnpm test |
Run all tests in watch mode |
pnpm test:unit |
Run unit tests only |
pnpm test:integration |
Run integration tests only (sequential) |
❗ Production build: The repo currently runs via tsx; add a
tscbuild +startscript before deploying to production environments like Vercel/Node runtime functions.
The API includes interactive documentation powered by Swagger UI and OpenAPI 3.0 (swagger-jsdoc and swagger-ui-express).
/api-docs (accessible when the server is running)All endpoints respond with { status, message, data? } JSON payloads.
For paginated endpoints (like GET /users and GET /roles), the response also includes meta and links objects containing paging data and HATEOAS navigational URLs. They optionally accept query parameters: ?page=1&limit=10&sortBy=createdAt&sortOrder=asc&search=value.
/auth)| Method | Path | Description | Auth | Validation |
|---|---|---|---|---|
POST |
/auth/register |
Register a new user and send verification email | Public | registerUserSchema |
POST |
/auth/login |
Verify credentials and return JWT token | Public | - |
POST |
/auth/logout |
Logout and revoke refresh token | Required | logoutSchema |
POST |
/auth/refresh-token |
Refresh access token using refresh token | Public | refreshTokenSchema |
POST |
/auth/verify-email |
Verify email address using token from email | Public | verifyEmailSchema |
POST |
/auth/resend-verification |
Resend verification email (rate limited: 3/10min) | Public | resendVerificationSchema |
POST |
/auth/forgot-password |
Request password reset email (rate limited: 3/15min) | Public | forgotPasswordSchema |
POST |
/auth/reset-password |
Reset password using token from email | Public | resetPasswordSchema |
/users)| Method | Path | Description | Auth | Validation |
|---|---|---|---|---|
POST |
/users |
Create a user (admin-style) | Required (users:create) |
createUserSchema |
GET |
/users |
List all users | Required (users:read) |
- |
GET |
/users/:id |
Fetch a user by ID | Required (users:read) |
- |
PUT |
/users/:id |
Update user fields (username, email, displayName) | Required (users:update) |
updateUserSchema |
PUT |
/users/:id/roles |
Assign roles to a user | Required (roles:assign) |
assignRolesSchema |
PATCH |
/users/password |
Update current user's password | Required (users:update) |
updatePasswordSchema |
DELETE |
/users/:id |
Remove a user | Required (users:delete) |
- |
/roles & /permissions)| Method | Path | Description | Auth | Validation |
|---|---|---|---|---|
GET |
/roles |
List all roles | Required (roles:read) |
- |
GET |
/roles/:id |
Fetch a role by ID | Required (roles:read) |
- |
POST |
/roles |
Create a new role | Required (roles:create) |
createRoleSchema |
PUT |
/roles/:id |
Update an existing role | Required (roles:update) |
updateRoleSchema |
DELETE |
/roles/:id |
Remove a role | Required (roles:delete) |
- |
GET |
/permissions |
List all system permissions | Required (roles:read) |
- |
Auth Required: Endpoints require Authorization: Bearer <token> header.
The API uses Zod for request validation with the following schemas:
registerUserSchema: Validates user registration (username, email, displayName, password)createUserSchema: Validates user creation (username, email, displayName)updateUserSchema: Validates user updates (partial fields)assignRolesSchema: Validates assigning role IDs to a userupdatePasswordSchema: Validates password changes (currentPassword, newPassword, confirmPassword)createRoleSchema: Validates new role details and array of permission IDsupdateRoleSchema: Validates partial updates to a roleverifyEmailSchema: Validates email verification tokenresendVerificationSchema: Validates email for resending verificationforgotPasswordSchema: Validates email for password reset requestsresetPasswordSchema: Validates reset token and new password (min 8 chars, uppercase, lowercase, number)refreshTokenSchema: Validates refresh token from body or cookieslogoutSchema: Validates optional refresh token for revocation on logoutAll schemas include:
authMiddleware (src/middleware/auth.middleware.ts): JWT verification and user authenticationrequireVerified (src/middleware/require-verified.middleware.ts): Ensures user email is verified before accessvalidate (src/middleware/validation.middleware.ts): Zod schema validation for request body/params/queryforgotPasswordLimiter (src/middleware/rate-limit.middleware.ts): Rate limiting for password reset (3 requests per 15 minutes)resendVerificationLimiter (src/middleware/rate-limit.middleware.ts): Rate limiting for resending verification emails (3 requests per 10 minutes)src/validations/<module>.validation.ts for request validation.src/routes/<module>.routes.ts and mount them in src/routes/index.ts.src/controllers/<module>.controller.ts.src/services/<module>.service.ts and reuse AppError for controlled failures.src/repositories/<module>.repository.ts.The API uses a custom AppError class for controlled error handling:
The API implements a robust, secure Refresh Token Rotation mechanism to safely extend user sessions without compromising security.
Refresh tokens are configured via environment variables in .env:
ENABLE_REFRESH_TOKEN=true
REFRESH_TOKEN_IN_JSON=true
REFRESH_TOKEN_IN_COOKIE=true
COOKIE_SAME_SITE="strict" # strict, lax, none
REFRESH_TOKEN_EXPIRY=604800000 # 7 days in milliseconds
/auth/refresh-token, their old refresh token is immediately revoked and a new pair is issued. This provides Replay Protection.HTTP-Only cookie (for web clients to prevent XSS) and/or directly in the JSON response body (for mobile apps or specialized clients).COOKIE_SAME_SITE variable. Use strict when the frontend and backend are on the exact same domain, or lax/none (along with secure: true) if operating across subdomains or entirely different domains.The API includes Cross-Origin Resource Sharing (CORS) support to allow requests from different origins (e.g., frontend applications).
CORS is configured via environment variables in .env:
# Comma-separated list of allowed origins
ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,http://localhost:8080,https://yourdomain.com
# Allow credentials (cookies, authorization headers)
CORS_CREDENTIALS=true
Content-Type, Authorization, X-Requested-With[!WARNING] Production Security
- Never use
*(wildcard) forALLOWED_ORIGINSin production- Only add trusted domains to the whitelist
- Use HTTPS for production origins (e.g.,
https://yourdomain.com)- Regularly audit the allowed origins list
[!IMPORTANT] Credentials Configuration
- Set
CORS_CREDENTIALS=trueonly if you're using cookies or need to send authorization headers- When credentials are enabled, you cannot use wildcard origins
- Frontend must include
credentials: 'include'(fetch) orwithCredentials: true(axios)
Development:
ALLOWED_ORIGINS=http://localhost:5173,http://localhost:3000,http://localhost:8080
CORS_CREDENTIALS=true
Production:
ALLOWED_ORIGINS=https://yourdomain.com,https://admin.yourdomain.com
CORS_CREDENTIALS=true
CORS Error: "No 'Access-Control-Allow-Origin' header"
ALLOWED_ORIGINSsrc/app.tsCredentials Not Working:
CORS_CREDENTIALS=true in .envcredentials: 'include' or withCredentials: true*)The API uses Pino for structured JSON logging with comprehensive audit trails:
Logged Events:
Log Format:
Example Log:
{
"level": 30,
"time": 1702890637123,
"action": "user_login",
"userId": "5ba52d7e-07f9-4b15-998f-fb1bf0885e7d",
"email": "user@example.com",
"msg": "User logged in successfully"
}
The project is configured to use tsup for efficient bundling.
pnpm builddist/src/server.ts and dependencies to dist/server.jsdist/views/emailspnpm startnode dist/server.jsbuild and start scripts in package.json.DATABASE_URL is set and migrations are run.Always run migrations in production before starting the app:
pnpm prisma migrate deploy
The project uses Vitest for unit and integration testing.
# Run all tests (watch mode)
pnpm test
# Run unit tests only
pnpm test:unit
# Run integration tests only
pnpm test:integration
# Run with coverage
pnpm exec vitest run --coverage
Tests are organized in tests/ with separate directories for unit and integration tests:
tests/
├── unit/ # Unit tests (mocked dependencies)
│ ├── controllers/
│ ├── services/
│ ├── middleware/
│ └── utils/
└── integration/ # Integration tests (real database)
├── helpers/ # Test utilities (DB reset)
├── repositories/ # Repository tests
└── routes/ # Route/endpoint tests
Integration tests run against a real PostgreSQL database. Ensure your DATABASE_URL points to a test database that can be safely cleared between tests.
[!WARNING] Integration tests truncate all tables before each test. Do not run against a production database.