Generated Project Structure
This page documents every file generated by xpress-generator create, what it does, and how it fits into the overall architecture.
MyApp/
βββ src/
β βββ app.js
β βββ server.js
β βββ config/
β β βββ config-{db}.js
β βββ shared/
β β βββ errors/
β β β βββ AppError.js
β β βββ utils/
β β β βββ catchAsync.js
β β β βββ httpResponse.js
β β βββ constants/
β β β βββ httpStatus.js
β β β βββ messages.js
β β βββ validators/
β β βββ validate.js
β β βββ exampleSchema.js
β βββ middleware/
β β βββ auth.js
β β βββ errorHandler.js
β βββ modules/
β βββ {name}/
β β βββ controller.js
β β βββ routes.js
β β βββ {name}Model.js
β β βββ service.js
β βββ auth/
β βββ authController.js
β βββ authRoutes.js
β βββ RefreshToken.js
βββ db/ (relational DBs only)
β βββ migrations/
β β βββ {timestamp}-create-{name}.js (.sql for SQL Server)
β βββ seeders/ (MySQL / PostgreSQL)
βββ tests/
β βββ setup.js
β βββ indexController.test.js
βββ .env
βββ .eslintrc.json
βββ .gitignore
βββ .husky/
β βββ pre-commit
βββ .lintstagedrc.json
βββ .sequelizerc (MySQL / PostgreSQL only)
βββ .xpress.json
βββ .dockerignore
βββ Dockerfile
βββ docker-compose.yml
βββ jest.config.js
βββ package.json
Entry pointsβ
src/server.jsβ
The actual entry point. Imports app and calls app.listen().
const app = require('./app');
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`π Server running on port ${PORT}`);
});
Why separate from app.js? So that tests can import app directly without binding to a port. See the Testing Guide.
src/app.jsβ
Configures Express: middleware stack, routes, 404 handler, global error handler. Exports module.exports = app without calling listen().
Configβ
src/config/config-{db}.jsβ
Database connection setup for your chosen DB. Exports a connection function that app.js calls on startup.
Shared utilities β src/shared/β
All code in src/shared/ is reusable across every module. It has no business logic β only infrastructure concerns.
src/shared/errors/AppError.jsβ
Custom error class for known, operational errors:
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.status = statusCode >= 500 ? 'error' : 'fail';
this.isOperational = true;
}
}
Use it anywhere in controllers or services:
throw new AppError('User not found', 404);
// or
next(new AppError('Unauthorized', 401));
src/shared/utils/catchAsync.jsβ
Wraps async route handlers so you never need to write try/catch in controllers:
module.exports = fn => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
src/shared/utils/httpResponse.jsβ
Standardizes all API responses:
httpResponse.success(res, data); // 200 { status: 'success', data }
httpResponse.created(res, data); // 201 { status: 'success', data }
httpResponse.noContent(res); // 204
httpResponse.paginated(res, data, meta); // 200 { status: 'success', data, meta }
src/shared/constants/httpStatus.jsβ
Named HTTP status codes to avoid magic numbers:
module.exports = {
OK: 200, CREATED: 201, NO_CONTENT: 204,
BAD_REQUEST: 400, UNAUTHORIZED: 401, FORBIDDEN: 403,
NOT_FOUND: 404, CONFLICT: 409, UNPROCESSABLE: 422,
SERVER_ERROR: 500
};
src/shared/constants/messages.jsβ
Reusable error message strings:
module.exports = {
NOT_FOUND: 'Resource not found',
UNAUTHORIZED: 'Invalid credentials',
TOKEN_REQUIRED: 'Access token required',
TOKEN_INVALID: 'Invalid or expired token',
FORBIDDEN: 'Insufficient permissions',
VALIDATION_ERROR: 'Validation failed'
};
src/shared/validators/validate.jsβ
Middleware factory. Takes a Zod schema and validates req.body:
const validate = require('../../shared/validators/validate');
const { loginSchema } = require('../../shared/validators/exampleSchema');
router.post('/login', validate(loginSchema), authController.login);
On failure, returns 422 with { status: 'fail', errors: [{ field, message }] }.
src/shared/validators/exampleSchema.jsβ
Example Zod schemas for name, email, and password.
Middleware β src/middleware/β
src/middleware/auth.jsβ
Exports three middleware functions:
| Export | Description |
|---|---|
verifyToken | Verifies Authorization: Bearer <token> |
requireRole(...roles) | Checks req.user.role against allowed roles |
authRateLimiter | Limits auth routes to 10 requests / 15 min / IP |
src/middleware/errorHandler.jsβ
Global error handler β the last middleware in app.js. Handles:
AppErrorinstances β{ status: 'fail', message }(4xx) or{ status: 'error', message }(5xx)- Mongoose
CastErrorβ 400 invalid ID - Mongoose
ValidationErrorβ 400 with field errors - Sequelize
UniqueConstraintErrorβ 409 conflict - JWT
JsonWebTokenErrorβ 401 invalid token - JWT
TokenExpiredErrorβ 401 expired token - Unknown errors in development β includes
stackin response
Modules β src/modules/β
Each feature lives in its own self-contained folder. The generated project starts with two modules.
src/modules/{name}/ β Index moduleβ
| File | Description |
|---|---|
controller.js | Handles GET / β returns a welcome response |
routes.js | Express router mounted on / in app.js |
{name}Model.js | The example model for your chosen database |
service.js | Business logic β getAll, getById, create, update, remove |
src/modules/auth/ β Auth moduleβ
| File | Description |
|---|---|
authController.js | Handles POST /api/auth/login, /refresh, /logout |
authRoutes.js | Auth router with authRateLimiter. Mounted on /api/auth |
RefreshToken.js | Stores refresh tokens (TTL index on MongoDB, table on SQL) |
Run xpress generate:model Product to scaffold a complete new module into src/modules/product/. See CLI Reference.
Migrations β db/β
Generated for relational databases only.
MySQL / PostgreSQLβ
| File | Description |
|---|---|
.sequelizerc | Points Sequelize CLI to src/config/sequelize.js and db/migrations/ |
src/config/sequelize.js | Sequelize CLI config β reads credentials from .env |
db/migrations/{ts}-create-{name}.js | Initial migration: creates the {name}s table |
db/seeders/ | Empty folder ready for seed data |
Run migrations:
npm run db:migrate # apply all pending
npm run db:migrate:undo # roll back last
SQL Serverβ
| File | Description |
|---|---|
db/migrations/{ts}-create-{name}.sql | Raw SQL with IF NOT EXISTS guard |
db/migrate.js | Custom runner β tracks applied files in _migrations table |
Run migrations:
npm run db:migrate
Dockerβ
| File | Description |
|---|---|
Dockerfile | Multi-stage: base β deps β dev β prod (+ builder for TypeScript) |
docker-compose.yml | App + DB service with healthchecks and named volumes |
.dockerignore | Excludes node_modules, .env, coverage, test files |
docker compose up # dev mode (hot-reload)
docker compose up --build # force rebuild
The production stage installs only production dependencies via npm ci --only=production β dev and test packages never reach the production image.
Testsβ
tests/setup.jsβ
Runs before every test suite. Sets up the test environment:
process.env.NODE_ENV = 'test';
process.env.PORT = '3001';
process.env.JWT_SECRET = 'test-jwt-secret-...';
process.env.REFRESH_TOKEN_SECRET = 'test-refresh-secret-...';
This ensures tests never need a real .env file and always run in a predictable environment.
tests/indexController.test.jsβ
Example integration test using supertest. Verifies GET /, unknown routes, and the auth endpoint.
Root filesβ
.xpress.jsonβ
{
"dbType": "MongoDB",
"language": "javascript",
"version": "1.0.0"
}
Used by generate:model and generate:middleware commands to auto-detect DB type and language.
.husky/pre-commitβ
npx lint-staged
Runs ESLint on staged files before every commit.
.lintstagedrc.jsonβ
{
"src/**/*.{js,ts}": ["eslint --fix"]
}