Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ async function askStackQuestions() {
{ name: chalk.bold.cyan("MEVN") + " → MongoDB + Express + Vue.js + Node.js", value: "mevn" },
{ name: chalk.bold.yellow("MEVN") + " + Tailwind + Auth", value: "mevn+tailwind+auth" },
{ name: chalk.bold.yellow("Next.js") + " + tRPC + Prisma + Tailwind + Auth", value: "t3-stack" },
{ name: chalk.bold.red("Hono") + " → Hono + Prisma + React", value: "hono" }
{ name: chalk.bold.red("Hono") + " → Hono + Prisma + React", value: "hono" },
{ name: chalk.bold.blue("Express TS Pro") + " → Production-ready TypeScript Express backend", value: "express-ts-pro" }
,{ name: chalk.bold.cyan("Next.js + Express") + " → Separate backend with Express.js", value: "next-express" }
,{ name: chalk.bold.green("MERN Turborepo") + " → Monorepo with Turbo (apps/client, apps/server)", value: "mern-turbo" }

],
pageSize: 10,
Expand Down
35 changes: 35 additions & 0 deletions templates/express-ts-pro/server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
Express TS Pro (Celtrix)

Production-ready TypeScript + Express backend template with:

- Logging (Winston) with JSON logs and request IDs
- Security: Helmet, CORS, rate limiting
- Role-based auth (JWT-based; roles via claims)
- Basic notification service (email via Nodemailer)
- File utilities: read XLSX and PDF
- Clean project structure and error handling

Quick Start

```
cp .env.example .env
npm install
npm run dev
```

Health check: GET `/health`

Scripts
- npm run dev: Run with ts-node + nodemon
- npm run build: Type-check and transpile to dist
- npm start: Run compiled app

Env
- PORT (default 4000)
- JWT_SECRET (required for auth)
- SMTP vars for notifications (optional)

Notes
- Upload endpoint: POST `/files/parse` (field: `file`) parses XLSX or PDF.
- Protected example: GET `/admin/stats` requires role `admin`.

38 changes: 38 additions & 0 deletions templates/express-ts-pro/server/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "express-ts-pro-server",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "nodemon --watch src --ext ts --exec ts-node src/server.ts",
"build": "tsc -p tsconfig.json",
"start": "node dist/server.js"
},
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-rate-limit": "^7.4.0",
"helmet": "^7.1.0",
"jsonwebtoken": "^9.0.2",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.14",
"pdf-parse": "^1.1.1",
"socket.io": "^4.8.1",
"useragent": "^2.3.0",
"uuid": "^9.0.1",
"winston": "^3.13.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/jsonwebtoken": "^9.0.6",
"@types/multer": "^1.4.12",
"@types/node": "^22.7.4",
"nodemon": "^3.1.7",
"ts-node": "^10.9.2",
"typescript": "^5.6.3"
}
}

28 changes: 28 additions & 0 deletions templates/express-ts-pro/server/src/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import { rateLimiter } from './setup/rateLimiter';
import { requestLogger } from './setup/requestLogger';
import { errorHandler, notFoundHandler } from './setup/errorHandlers';
import { router as apiRouter } from './setup/routes';

export function createApp() {
const app = express();

app.use(helmet());
app.use(cors());
app.use(express.json({ limit: '2mb' }));
app.use(requestLogger);
app.use(rateLimiter);

app.get('/health', (_req, res) => res.json({ status: 'ok' }));

app.use('/api/v1', apiRouter);

app.use(notFoundHandler);
app.use(errorHandler);

return app;
}


27 changes: 27 additions & 0 deletions templates/express-ts-pro/server/src/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'dotenv/config';
import { createApp } from './app';
import { logger } from './setup/logger';
import { createServer } from 'http';
import { Server } from 'socket.io';
import { MonitoringHub } from './telemetry/monitoringHub';

const app = createApp();
const httpServer = createServer(app);
const io = new Server(httpServer, { cors: { origin: '*' } });
MonitoringHub.attach(io);

const port = Number(process.env.PORT) || 4000;
const server = httpServer.listen(port, () => {
logger.info('server_started', { port, env: process.env.NODE_ENV || 'development' });
});

process.on('unhandledRejection', (reason: any) => {
logger.error('unhandled_rejection', { reason: reason?.message || String(reason) });
});

process.on('uncaughtException', (err) => {
logger.error('uncaught_exception', { message: err.message, stack: err.stack });
server.close(() => process.exit(1));
});


14 changes: 14 additions & 0 deletions templates/express-ts-pro/server/src/setup/errorHandlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Request, Response, NextFunction } from 'express';
import { logger } from './logger';

export function notFoundHandler(_req: Request, res: Response) {
res.status(404).json({ error: 'Not found' });
}

export function errorHandler(err: any, _req: Request, res: Response, _next: NextFunction) {
logger.error('unhandled_error', { message: err?.message, stack: err?.stack });
const status = typeof err?.status === 'number' ? err.status : 500;
res.status(status).json({ error: err?.message || 'Internal Server Error' });
}


7 changes: 7 additions & 0 deletions templates/express-ts-pro/server/src/setup/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export * from './errorHandlers';
export * from './logger';
export * from './rateLimiter';
export * from './requestLogger';
export * from './routes';


37 changes: 37 additions & 0 deletions templates/express-ts-pro/server/src/setup/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { createLogger, format, transports } from 'winston';
import path from 'path';
import fs from 'fs';
import { v4 as uuid } from 'uuid';
import { Request, Response, NextFunction } from 'express';

const logDir = path.join(process.cwd(), 'src', 'log');
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });

export const logger = createLogger({
level: 'info',
format: format.combine(
format.timestamp(),
format.errors({ stack: true }),
format.json(),
),
transports: [
new transports.Console({
format: format.combine(
format.colorize(),
format.printf(({ level, message, timestamp, ...meta }) => {
return `${timestamp} ${level}: ${message} ${Object.keys(meta).length ? JSON.stringify(meta) : ''}`;
})
)
}),
new transports.File({ filename: path.join(logDir, 'error.log'), level: 'error' }),
new transports.File({ filename: path.join(logDir, 'warning.log'), level: 'warn' }),
new transports.File({ filename: path.join(logDir, 'success.log'), level: 'info' })
],
});

export function withRequestContext(req: Request, _res: Response, next: NextFunction) {
(req as any).requestId = (req.headers['x-request-id'] as string) || uuid();
next();
}


10 changes: 10 additions & 0 deletions templates/express-ts-pro/server/src/setup/rateLimiter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import rateLimit from 'express-rate-limit';

export const rateLimiter = rateLimit({
windowMs: 60 * 1000,
limit: 100,
standardHeaders: true,
legacyHeaders: false,
});


30 changes: 30 additions & 0 deletions templates/express-ts-pro/server/src/setup/requestLogger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Request, Response, NextFunction } from 'express';
import { logger } from './logger';
import useragent from 'useragent';
import { MonitoringHub } from '../telemetry/monitoringHub';

export function requestLogger(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
res.on('finish', () => {
const durationMs = Date.now() - start;
const agent = useragent.parse(req.headers['user-agent'] || '');
const payload = {
method: req.method,
path: req.originalUrl,
status: res.statusCode,
durationMs,
ip: req.ip,
ua: {
family: agent.family,
os: agent.os?.toString(),
device: agent.device?.toString()
},
timestamp: new Date().toISOString()
};
logger.info('http_request', payload);
MonitoringHub.emitEvent('http_request', payload);
});
next();
}


17 changes: 17 additions & 0 deletions templates/express-ts-pro/server/src/setup/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Router } from 'express';
import { authenticate, requireRoles } from '../system/auth';
import { router as filesRouter } from '../system/files';
import { router as notifyRouter } from '../system/notify';
import { dashboardRouter } from '../telemetry/dashboard';

export const router = Router();

router.use('/files', authenticate, filesRouter);
router.use('/notify', authenticate, notifyRouter);
router.use('/monitor', dashboardRouter);

router.get('/admin/stats', authenticate, requireRoles('admin'), (_req, res) => {
res.json({ users: 0, uptime: process.uptime() });
});


40 changes: 40 additions & 0 deletions templates/express-ts-pro/server/src/system/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';

export type JwtUser = {
sub: string;
roles?: string[];
};

declare global {
namespace Express {
interface Request {
user?: JwtUser;
}
}
}

export function authenticate(req: Request, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization || '';
const token = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : undefined;
if (!token) return res.status(401).json({ error: 'Missing token' });
try {
const secret = process.env.JWT_SECRET || 'dev_secret';
const decoded = jwt.verify(token, secret) as JwtUser;
req.user = decoded;
next();
} catch {
return res.status(401).json({ error: 'Invalid token' });
}
}

export function requireRoles(...roles: string[]) {
return (req: Request, res: Response, next: NextFunction) => {
const userRoles = req.user?.roles || [];
const ok = roles.every((r) => userRoles.includes(r));
if (!ok) return res.status(403).json({ error: 'Forbidden' });
next();
};
}


52 changes: 52 additions & 0 deletions templates/express-ts-pro/server/src/system/files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Router } from 'express';
import multer from 'multer';
import path from 'path';
import fs from 'fs';
import xlsx from 'xlsx';
import pdfParse from 'pdf-parse';

export const router = Router();

const diskStorage = multer.diskStorage({
destination: (_req, file, cb) => {
const mime = file.mimetype;
let folder = 'documents';
if (mime.startsWith('image/')) folder = 'images';
else if (mime.startsWith('video/')) folder = 'videos';
const uploadDir = path.join(process.cwd(), 'uploads', folder);
fs.mkdirSync(uploadDir, { recursive: true });
cb(null, uploadDir);
},
filename: (_req, file, cb) => {
const unique = Date.now() + '-' + Math.round(Math.random() * 1e9);
cb(null, unique + path.extname(file.originalname));
}
});

const upload = multer({ storage: diskStorage, limits: { fileSize: 10 * 1024 * 1024 } });

router.post('/parse', upload.single('file'), async (req, res, next) => {
try {
if (!req.file) return res.status(400).json({ error: 'file is required' });
const mime = req.file.mimetype;
if (mime === 'application/pdf') {
const data = await pdfParse(req.file.buffer);
return res.json({ type: 'pdf', text: data.text.slice(0, 5000) });
}
if (
mime === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ||
mime === 'application/vnd.ms-excel' ||
mime === 'application/octet-stream'
) {
const wb = xlsx.readFile(req.file.path);
const first = wb.SheetNames[0];
const rows = xlsx.utils.sheet_to_json(wb.Sheets[first], { header: 1 });
return res.json({ type: 'xlsx', rows });
}
return res.status(415).json({ error: 'Unsupported file type' });
} catch (e) {
next(e);
}
});


Loading