Skip to main content

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:

ExportDescription
verifyTokenVerifies Authorization: Bearer <token>
requireRole(...roles)Checks req.user.role against allowed roles
authRateLimiterLimits auth routes to 10 requests / 15 min / IP

src/middleware/errorHandler.js​

Global error handler β€” the last middleware in app.js. Handles:

  • AppError instances β†’ { 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 stack in 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​

FileDescription
controller.jsHandles GET / β€” returns a welcome response
routes.jsExpress router mounted on / in app.js
{name}Model.jsThe example model for your chosen database
service.jsBusiness logic β€” getAll, getById, create, update, remove

src/modules/auth/ β€” Auth module​

FileDescription
authController.jsHandles POST /api/auth/login, /refresh, /logout
authRoutes.jsAuth router with authRateLimiter. Mounted on /api/auth
RefreshToken.jsStores refresh tokens (TTL index on MongoDB, table on SQL)
Adding a new module

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​

FileDescription
.sequelizercPoints Sequelize CLI to src/config/sequelize.js and db/migrations/
src/config/sequelize.jsSequelize CLI config β€” reads credentials from .env
db/migrations/{ts}-create-{name}.jsInitial 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​

FileDescription
db/migrations/{ts}-create-{name}.sqlRaw SQL with IF NOT EXISTS guard
db/migrate.jsCustom runner β€” tracks applied files in _migrations table

Run migrations:

npm run db:migrate

Docker​

FileDescription
DockerfileMulti-stage: base β†’ deps β†’ dev β†’ prod (+ builder for TypeScript)
docker-compose.ymlApp + DB service with healthchecks and named volumes
.dockerignoreExcludes 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"]
}