create-xpress-backend
v1.0.2
Published
Scaffold a production-ready Express + Prisma + PostgreSQL backend in seconds
Maintainers
Readme
create-xpress-backend
Scaffold a production-ready Express + Prisma + PostgreSQL backend in seconds.
Choose TypeScript (default) or JavaScript — everything wired up, zero boilerplate.
npx create-xpress-backendTable of Contents
- Requirements
- How to Use the CLI
- TypeScript vs JavaScript
- What Gets Scaffolded
- Project Structure Explained
- Every File — What It Does
- Environment Variables
- First-Time Setup (Step by Step)
- Available Scripts
- API Reference
- How the Architecture Works
- Adding a New Module
- File Upload System
- JWT Auth System
- Database Workflows
- Deployment Checklist
- FAQ
Requirements
| Requirement | Minimum | Check |
|-------------|---------|-------|
| Node.js | ≥ 18.0.0 | node -v |
| npm | ≥ 8.0.0 | npm -v |
| PostgreSQL | ≥ 13 | running locally or on a cloud service |
PostgreSQL can be local, Docker, or a hosted service — Supabase, Neon, Railway, Render, etc.
How to Use the CLI
No global install needed. Just run:
npx create-xpress-backendYou will see the banner and then a short series of prompts:
┌─────────────────────────────────────────────┐
│ 🚀 create-xpress-backend v1.0.0 │
│ Express · Prisma · PostgreSQL · Node.js │
└─────────────────────────────────────────────┘
project_name : my-api
language : ❯ TypeScript (recommended)
JavaScript
port : 5050
file upload system : include? (Y/n)
JWT auth module : include? (y/N)
git init : initialize? (Y/n)Prompt Reference
| Prompt | What it does | Default |
|--------|-------------|---------|
| project_name | Folder name + npm package name. Lowercase letters, numbers, hyphens, underscores only. | — |
| language | TypeScript or JavaScript. Arrow-key selection. | TypeScript |
| port | Port written into .env and used in startup logs. | 5050 |
| file upload system | Adds Multer, an /uploads directory, and upload/list/delete routes. | Yes |
| JWT auth module | Adds register + login endpoints and a JWT guard middleware. | No |
| git init | Runs git init inside the generated folder. | Yes |
After confirming, the CLI scaffolds everything and prints:
✔ Done! Project "my-api" created! (TypeScript)
Get started:
cd my-api
npm install
# configure DATABASE_URL in .env, then:
npm run db:push
npm run dev
Build for production:
npm run build
npm start
Health check → http://localhost:5050/health
Prisma Studio → npm run db:studioTypeScript vs JavaScript
The CLI generates a complete, properly typed codebase for whichever language you pick. There is no "TS wrapper around JS" — every file is written natively.
| | TypeScript | JavaScript |
|--|-----------|-----------|
| Source files | .ts | .js |
| Module system | ES module imports (import/export) | CommonJS (require/module.exports) |
| Dev server | ts-node-dev | nodemon |
| Build step | tsc → dist/ | None — runs directly |
| Start (prod) | node dist/server.js | node src/server.js |
| Type checking | ✅ Strict mode | — |
| tsconfig.json | ✅ Included | — |
| src/types/express.d.ts | ✅ Included (req.user typed) | — |
| Type packages | @types/* included | — |
Recommendation: Use TypeScript. It catches bugs at compile time and makes refactoring safer — especially important as your API grows.
What Gets Scaffolded
Depending on your choices, between 17 and 25 files are generated:
| Language | Base | + Upload | + Auth | + Both | |----------|------|----------|--------|--------| | TypeScript | 19 | 20 | 24 | 25 | | JavaScript | 17 | 18 | 22 | 23 |
Project Structure Explained
TypeScript
my-api/
├── prisma/
│ └── schema.prisma ← database models
├── src/
│ ├── app.ts ← Express config (middleware, routes, error handler)
│ ├── server.ts ← HTTP server + graceful shutdown
│ ├── types/
│ │ └── express.d.ts ← augments req.user with JwtPayload type
│ ├── database/
│ │ ├── prisma.ts ← shared PrismaClient singleton
│ │ └── seed.ts ← database seeder
│ ├── middleware/
│ │ ├── validate.ts ← Joi validation wrapper
│ │ ├── upload.ts ← Multer config (if uploads selected)
│ │ └── auth.ts ← JWT guard middleware (if auth selected)
│ └── modules/
│ ├── index.ts ← central router
│ ├── demo/ ← example CRUD module
│ │ ├── demo.controller.ts
│ │ ├── demo.service.ts
│ │ ├── demo.routes.ts
│ │ └── demo.validation.ts
│ └── auth/ ← auth module (if auth selected)
│ ├── auth.controller.ts
│ ├── auth.service.ts
│ ├── auth.routes.ts
│ └── auth.validation.ts
├── uploads/ ← file storage (if uploads selected)
├── tsconfig.json
├── prisma.config.ts
├── .env
├── .env.example
├── .gitignore
└── package.jsonJavaScript
Same structure but with .js extensions, require/module.exports, no tsconfig.json, no types/ folder.
Every File — What It Does
src/server.ts / src/server.js
Entry point. Creates an http.Server around the Express app, starts listening on PORT, logs startup info, and handles SIGINT/SIGTERM for graceful shutdown with a 5-second forced-exit fallback.
src/app.ts / src/app.js
Configures the Express app — does not start the server. Middleware stack applied in order:
helmet()— secure HTTP headerscors()— CORS fromCLIENT_URLmorgan('dev')— request loggingexpress.json()— JSON body parsing (10 MB limit)express.urlencoded()— form body parsing/uploadsstatic route — serves uploaded files (if uploads enabled)/api— all module routes/health— health check- 404 handler
- Central error handler — anything passed to
next(error)lands here
src/database/prisma.ts / prisma.js
Creates and exports a single shared PrismaClient using the @prisma/adapter-pg driver adapter with a pg connection pool. Every module imports from here.
src/database/seed.ts / seed.js
Standalone seeder. Safe to run multiple times — checks for existing records before inserting. Extend this to seed any initial data your app needs.
prisma/schema.prisma
Defines your database models. Base includes User. If file uploads were selected, File is also included with a User relation. Edit here, then run npm run db:push.
src/types/express.d.ts (TypeScript only)
Augments Express's Request interface to add user?: string | JwtPayload. This is what makes req.user type-safe in every controller and middleware.
src/middleware/validate.ts / validate.js
Takes a Joi schema, returns Express middleware. On failure, returns 400 with the first validation error. Usage:
router.post('/', validate(createSchema), controller.create);src/middleware/upload.ts / upload.js (if uploads selected)
Multer configured with disk storage, unique filenames, file type filtering (JPEG/PNG/GIF/PDF/TXT/DOC/DOCX), and size limit from MAX_FILE_SIZE env var.
src/middleware/auth.ts / auth.js (if auth selected)
Reads Authorization: Bearer <token>, verifies against JWT_SECRET, attaches decoded payload to req.user. Returns 401 for missing or invalid tokens.
router.get('/me', authMiddleware, controller.getMe);src/modules/index.ts / index.js
The central router. Add one line per new module:
router.use('/users', usersRoutes);Demo Module (src/modules/demo/)
Working example of the Controller → Service → Routes → Validation pattern:
- Validation — Joi schemas for create and update
- Routes — maps HTTP verbs + paths to controller methods, attaches middleware
- Controller — handles
req/res, calls service, passes errors tonext() - Service — all Prisma calls live here, returns plain data, throws on error
This is your template module. Copy it, rename everything, and replace the Prisma calls for your first real feature.
Auth Module (src/modules/auth/) (if selected)
- Register — hashes password (bcrypt, 10 rounds), creates user, returns user without password field
- Login — finds user, compares password, signs JWT, returns
{ user, token }
Environment Variables
Your .env is pre-filled when the project is created:
NODE_ENV=development
PORT=5050
# Database — edit to match your Postgres instance
DATABASE_URL="postgresql://postgres:admin@localhost:5432/my_api_db"
# Format: postgresql://USER:PASSWORD@HOST:PORT/DATABASE_NAME
# JWT — change JWT_SECRET to a long random string before deploying
JWT_SECRET=your-super-secret-jwt-key-change-this
JWT_EXPIRES_IN=7d # 7d | 24h | 3600 (seconds)
# File Upload (if included)
MAX_FILE_SIZE=10 # megabytes
# CORS
CLIENT_URL=http://localhost:3000
SERVER_URL=http://localhost:5050
.envis git-ignored. Commit.env.example— it shows required keys without real values.
First-Time Setup (Step by Step)
# 1. Scaffold
npx create-xpress-backend
# 2. Enter your project
cd my-api
# 3. Install dependencies
npm install
# 4. Create the database (if it doesn't exist yet)
# psql -U postgres -c "CREATE DATABASE my_api_db;"
# 5. Set DATABASE_URL in .env
# DATABASE_URL="postgresql://postgres:yourpassword@localhost:5432/my_api_db"
# 6. Push schema to DB + generate Prisma Client
npm run db:push
# 7. (Optional) Seed initial data
npm run db:seed
# 8. Start dev server
npm run devVisit http://localhost:5050/health:
{
"status": "OK",
"message": "API Running ✅",
"timestamp": "2026-07-09T10:00:00.000Z",
"uptime": 3.2
}Available Scripts
TypeScript project
| Command | Description |
|---------|-------------|
| npm run dev | Start with ts-node-dev (auto-restarts + transpiles on save) |
| npm run build | Compile TypeScript → dist/ |
| npm start | Run compiled production build (node dist/server.js) |
| npm run db:push | Push schema changes + regenerate Prisma Client |
| npm run db:generate | Regenerate Prisma Client without pushing |
| npm run db:dev | db:push + db:generate in one command |
| npm run db:seed | Run the database seeder |
| npm run db:studio | Open Prisma Studio |
| npm run db:reset | ⚠️ Force-reset database (drops all data) |
JavaScript project
Same as above except:
npm run devusesnodemon- No
npm run build— runs directly npm startrunsnode src/server.js
API Reference
Health
GET /health{ "status": "OK", "message": "API Running ✅", "timestamp": "...", "uptime": 120.4 }Demo — CRUD (always included)
Create
POST /api/demo
Content-Type: application/json
{ "name": "Alice", "email": "[email protected]" }Returns 201 with the created record.
Get All
GET /api/demoReturns 200 with array of records (newest first).
Get by ID
GET /api/demo/:idReturns 200 with the record, or 404.
Update
PUT /api/demo/:id
Content-Type: application/json
{ "name": "Alice Updated" }Returns 200 with updated record.
Delete
DELETE /api/demo/:idReturns 200 with { "message": "Demo deleted successfully" }.
File Upload (if selected)
Upload
POST /api/demo/upload
Content-Type: multipart/form-data
field: fileReturns 201 with the stored file record.
List
GET /api/demo/files/allReturns 200 with array of file records.
Delete
DELETE /api/demo/files/:fileIdDeletes physical file from disk + database record.
Allowed types: JPEG, PNG, GIF, PDF, TXT, DOC, DOCX
Max size: MAX_FILE_SIZE MB (default 10)
Auth (if selected)
Register
POST /api/auth/register
Content-Type: application/json
{ "name": "Alice", "email": "[email protected]", "password": "secret123" }Returns 201 with user object (no password field).
Login
POST /api/auth/login
Content-Type: application/json
{ "email": "[email protected]", "password": "secret123" }Returns 200 with:
{
"success": true,
"data": {
"user": { "id": "...", "name": "Alice", "email": "[email protected]" },
"token": "eyJhbGci..."
}
}Use the token on protected routes:
Authorization: Bearer eyJhbGci...How the Architecture Works
Every request flows through the same layers in the same order:
Incoming Request
│
▼
Middleware Stack (app.ts)
helmet → cors → morgan → body-parser
│
▼
Module Router (src/modules/index.ts)
/api/demo → demo.routes.ts
/api/auth → auth.routes.ts
│
▼
Route (*.routes.ts)
validate(schema) → authMiddleware → controller.method
│
▼
Controller (*.controller.ts)
calls service → formats JSON → passes errors to next()
│
▼
Service (*.service.ts)
all Prisma / database logic
throws typed errors
│
▼
Central Error Handler (app.ts)
catches everything passed to next(error)
returns { success: false, message }Key rules:
- Controllers never touch the database — that belongs in the service
- Services never touch
req/res— they work with plain data and types - Validation runs before the controller — invalid requests are rejected early
- All errors bubble to one place — the central error handler in
app.ts
Adding a New Module
Example: adding a products module.
1. Add a Prisma model
model Product {
id String @id @default(uuid())
name String
price Float
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}2. Push the schema
npm run db:push3. Create the module folder
src/modules/products/
├── products.validation.ts
├── products.service.ts
├── products.controller.ts
└── products.routes.ts4. Write the service (Prisma calls go here)
import prisma from '../../database/prisma';
class ProductsService {
async getAll() {
return prisma.product.findMany({ orderBy: { createdAt: 'desc' } });
}
}
export default new ProductsService();5. Register the route in src/modules/index.ts
import productsRoutes from './products/products.routes';
router.use('/products', productsRoutes);GET /api/products is now live.
File Upload System
Client → multipart/form-data (field: "file")
│
▼
upload.single('file') ← Multer middleware
validates type + size
writes to uploads/<name>-<timestamp>-<random>.<ext>
attaches to req.file
│
▼
Controller reads req.file
passes userId + file to service
│
▼
Service creates File record in database
stores: filename, originalName, path, size, mimetype, userId
│
▼
File on disk: uploads/file-1720524000000-123456789.jpg
DB record in: files table
Accessible at: GET /uploads/<filename>To restrict file types, edit allowedMimes in src/middleware/upload.ts.
To change max size, update MAX_FILE_SIZE in .env (value in MB).
JWT Auth System
POST /api/auth/register
validate { name, email, password }
check if email exists → 409 if yes
bcrypt.hash(password, 10)
prisma.user.create(...)
return user (password stripped)
POST /api/auth/login
validate { email, password }
find user by email → 401 if not found
bcrypt.compare(...) → 401 if wrong
jwt.sign({ id, email }, JWT_SECRET, { expiresIn })
return { user, token }
Protected Route
Authorization: Bearer <token>
authMiddleware verifies token
attaches decoded payload → req.user
→ 401 if missing or invalidTo protect a route:
// TypeScript
import authMiddleware from '../../middleware/auth';
router.get('/me', authMiddleware, controller.getMe.bind(controller));// JavaScript
const authMiddleware = require('../../middleware/auth');
router.get('/me', authMiddleware, controller.getMe);Inside the controller, req.user will contain { id, email, iat, exp }.
Database Workflows
Iterating on your schema
# 1. Edit prisma/schema.prisma
# 2. Push + regenerate
npm run db:push
# 3. Browse your data
npm run db:studioReset for a clean slate (dev only)
npm run db:reset # ⚠️ drops all data
npm run db:seed # re-seed if neededConnecting to a hosted database
# Supabase
DATABASE_URL="postgresql://postgres:[password]@db.[ref].supabase.co:5432/postgres"
# Neon
DATABASE_URL="postgresql://[user]:[password]@[host].neon.tech/[db]?sslmode=require"
# Railway
DATABASE_URL="postgresql://postgres:[password]@[host].railway.app:5432/railway"Deployment Checklist
- [ ]
NODE_ENV=productionset in hosting environment - [ ]
JWT_SECRETis a long, random, unique string — never the default - [ ]
CLIENT_URLset to your real frontend domain (not*) - [ ]
DATABASE_URLpoints to production database - [ ]
npm run db:pushrun against the production database - [ ] TypeScript only:
npm run buildrun before deploy — serve fromdist/ - [ ]
uploads/is on persistent storage (not ephemeral filesystem) - [ ] Demo routes removed or protected before go-live
Production start:
# TypeScript
npm run build && npm start
# JavaScript
npm startMinimal Dockerfile (TypeScript):
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
EXPOSE 5050
CMD ["npm", "start"]FAQ
Q: Which should I pick — TypeScript or JavaScript?
A: TypeScript is the default for a reason. Strict typing catches bugs before runtime, makes refactoring reliable, and improves IDE autocomplete. If you're already comfortable with TS, pick it. If you're learning or need a quick prototype, JS is perfectly fine.
Q: What does ts-node-dev do?
A: It's the TypeScript equivalent of nodemon — it watches .ts files, transpiles them on the fly, and restarts the server on changes. No manual tsc step during development.
Q: Do I need to run npm run build during development?
A: No. npm run dev uses ts-node-dev which runs TypeScript directly. npm run build is only needed when preparing for production.
Q: Can I use MySQL or SQLite instead of PostgreSQL?
A: The generated project uses @prisma/adapter-pg (PostgreSQL-specific). To switch, remove the adapter, change provider in schema.prisma, and replace the pg pool. Prisma supports MySQL, SQLite, MongoDB, and others.
Q: Why is there a demo module instead of a real resource?
A: It's intentionally generic so it doesn't clash with your actual domain models. It shows the pattern — rename and replace it for your first real feature.
Q: How do I add the password field if I didn't choose JWT auth?
A: Add it to the User model in schema.prisma:
model User {
...
password String?
}Then run npm run db:push.
Q: What is prisma.config.ts / prisma.config.js for?
A: Prisma 7 introduced a new config file format. It tells the Prisma CLI where to find DATABASE_URL. You don't need to edit it.
License
MIT © ayushsolanki29
