@re-shell/cli
Version:
Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja
1,898 lines (1,618 loc) • 68.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.restifyTemplate = void 0;
exports.restifyTemplate = {
id: 'restify',
name: 'restify',
displayName: 'Restify',
description: 'Optimized for building REST APIs with built-in throttling, DTrace support, and API versioning',
language: 'typescript',
framework: 'restify',
version: '11.1.0',
tags: ['nodejs', 'restify', 'api', 'rest', 'microservices', 'throttling', 'typescript'],
port: 3000,
dependencies: {},
features: ['api-versioning', 'throttling', 'request-validation', 'bunyan-logging', 'dtrace', 'error-handling', 'authentication', 'swagger'],
files: {
// TypeScript project configuration
'package.json': `{
"name": "{{projectName}}",
"version": "1.0.0",
"description": "Restify REST API server with TypeScript",
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/index.ts | bunyan",
"build": "tsc",
"start": "node dist/index.js | bunyan",
"start:prod": "cross-env NODE_ENV=production node dist/index.js | bunyan",
"lint": "eslint src --ext .ts",
"test": "mocha --require ts-node/register --require source-map-support/register --recursive 'test/**/*.spec.ts'",
"test:watch": "mocha --watch --require ts-node/register --recursive 'test/**/*.spec.ts'",
"test:coverage": "nyc npm test",
"typecheck": "tsc --noEmit",
"format": "prettier --write .",
"migrate": "node dist/scripts/migrate.js",
"docker:build": "docker build -t {{projectName}} .",
"docker:run": "docker run -p 3000:3000 {{projectName}}"
},
"dependencies": {
"restify": "^11.1.0",
"restify-errors": "^8.0.2",
"restify-cors-middleware2": "^2.2.1",
"restify-router": "^0.6.2",
"restify-jwt-community": "^2.0.2",
"restify-swagger-jsdoc": "^3.5.0",
"bunyan": "^1.8.15",
"joi": "^17.12.3",
"jsonwebtoken": "^9.0.2",
"bcryptjs": "^2.4.3",
"dotenv": "^16.4.5",
"pg": "^8.11.5",
"pg-pool": "^3.6.2",
"redis": "^4.6.13",
"ioredis": "^5.3.2",
"uuid": "^9.0.1",
"lodash": "^4.17.21",
"dayjs": "^1.11.10",
"axios": "^1.6.8",
"nodemailer": "^6.9.13",
"pino": "^9.0.0",
"pino-pretty": "^11.0.0",
"helmet": "^7.1.0",
"compression": "^1.7.4",
"multer": "^1.4.5-lts.1",
"sharp": "^0.33.3",
"bull": "^4.12.2",
"node-cron": "^3.0.3",
"socket.io": "^4.7.5",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0"
},
"devDependencies": {
"@types/restify": "^8.5.12",
"@types/restify-errors": "^4.3.9",
"@types/restify-cors-middleware": "^1.0.5",
"@types/bunyan": "^1.8.11",
"@types/joi": "^17.2.3",
"@types/node": "^20.12.7",
"@types/bcryptjs": "^2.4.6",
"@types/jsonwebtoken": "^9.0.6",
"@types/pg": "^8.11.5",
"@types/lodash": "^4.17.0",
"@types/nodemailer": "^6.4.14",
"@types/multer": "^1.4.11",
"@types/passport": "^1.0.16",
"@types/passport-jwt": "^4.0.1",
"@types/passport-local": "^1.0.38",
"@types/bull": "^4.10.0",
"@types/node-cron": "^3.0.11",
"@types/mocha": "^10.0.6",
"@types/chai": "^4.3.14",
"@types/supertest": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^7.7.1",
"@typescript-eslint/parser": "^7.7.1",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"prettier": "^3.2.5",
"typescript": "^5.4.5",
"tsx": "^4.7.2",
"cross-env": "^7.0.3",
"mocha": "^10.4.0",
"chai": "^4.4.1",
"chai-http": "^4.4.0",
"supertest": "^7.0.0",
"sinon": "^17.0.1",
"@types/sinon": "^17.0.3",
"nyc": "^15.1.0",
"ts-node": "^10.9.2",
"source-map-support": "^0.5.21",
"nodemon": "^3.1.0"
},
"nyc": {
"extension": [
".ts"
],
"exclude": [
"**/*.d.ts",
"coverage",
"dist",
"test"
],
"reporter": [
"html",
"text",
"lcov"
],
"all": true
}
}`,
// TypeScript configuration
'tsconfig.json': `{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": true,
"noEmitOnError": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@config/*": ["src/config/*"],
"@controllers/*": ["src/controllers/*"],
"@middlewares/*": ["src/middlewares/*"],
"@models/*": ["src/models/*"],
"@routes/*": ["src/routes/*"],
"@services/*": ["src/services/*"],
"@utils/*": ["src/utils/*"],
"@validators/*": ["src/validators/*"],
"@types/*": ["src/types/*"]
}
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "coverage", "test"]
}`,
// Main application entry point
'src/index.ts': `import * as restify from 'restify';
import corsMiddleware from 'restify-cors-middleware2';
import { config } from './config/config';
import { logger } from './utils/logger';
import { setupRoutes } from './routes';
import { errorHandler } from './middlewares/error.middleware';
import { requestLogger } from './middlewares/logger.middleware';
import { setupSwagger } from './config/swagger';
import { connectDatabase } from './config/database';
import { redisClient } from './config/redis';
import { initializeWebSocket } from './config/websocket';
import { gracefulShutdown } from './utils/gracefulShutdown';
// Create server
const server = restify.createServer({
name: config.appName,
version: config.version,
log: logger,
handleUncaughtExceptions: true
});
// Initialize WebSocket
const io = initializeWebSocket(server.server);
// Pre-routing middleware
server.pre(restify.pre.sanitizePath());
server.pre(restify.pre.userAgentConnection());
// CORS configuration
const cors = corsMiddleware({
origins: config.corsOrigins,
credentials: true,
allowHeaders: ['Authorization', 'Content-Type'],
exposeHeaders: ['X-Request-ID']
});
server.pre(cors.preflight);
server.use(cors.actual);
// Body parsing
server.use(restify.plugins.acceptParser(server.acceptable));
server.use(restify.plugins.queryParser({ mapParams: true }));
server.use(restify.plugins.bodyParser({ mapParams: true }));
server.use(restify.plugins.gzipResponse());
// Request tracking
server.use(restify.plugins.requestLogger());
// Throttling
server.use(restify.plugins.throttle({
burst: 100,
rate: 50,
ip: true,
overrides: {
'127.0.0.1': {
rate: 0,
burst: 0
}
}
}));
// Custom middleware
server.use(requestLogger);
// Health check endpoint
server.get('/health', (req, res, next) => {
res.send({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: config.env,
version: config.version
});
next();
});
// API routes
setupRoutes(server);
// API documentation
setupSwagger(server);
// Error handling
server.on('restifyError', errorHandler);
server.on('uncaughtException', (req, res, route, err) => {
logger.error({ err, req, route }, 'Uncaught exception');
res.send(500, { error: 'Internal server error' });
});
// Graceful shutdown handlers
process.on('SIGTERM', () => gracefulShutdown(server, redisClient));
process.on('SIGINT', () => gracefulShutdown(server, redisClient));
// Start server
const startServer = async () => {
try {
// Connect to database
await connectDatabase();
// Connect to Redis
await redisClient.connect();
server.listen(config.port, config.host, () => {
logger.info(\`🚀 Server is running on http://\${config.host}:\${config.port}\`);
logger.info(\`📚 API Documentation: http://\${config.host}:\${config.port}/api-docs\`);
logger.info(\`🔧 Environment: \${config.env}\`);
logger.info(\`📊 Metrics: http://\${config.host}:\${config.port}/metrics\`);
});
} catch (error) {
logger.fatal('Failed to start server:', error);
process.exit(1);
}
};
startServer();
export { server, io };`,
// Configuration
'src/config/config.ts': `import * as dotenv from 'dotenv';
import * as joi from 'joi';
dotenv.config();
// Define validation schema
const envSchema = joi.object({
NODE_ENV: joi.string().valid('development', 'production', 'test').default('development'),
PORT: joi.number().default(3000),
HOST: joi.string().default('0.0.0.0'),
APP_NAME: joi.string().default('{{projectName}}'),
VERSION: joi.string().default('1.0.0'),
// Database
DATABASE_URL: joi.string().required(),
DB_POOL_MIN: joi.number().default(2),
DB_POOL_MAX: joi.number().default(10),
// Redis
REDIS_URL: joi.string().default('redis://localhost:6379'),
// JWT
JWT_SECRET: joi.string().required(),
JWT_EXPIRE: joi.string().default('7d'),
JWT_REFRESH_EXPIRE: joi.string().default('30d'),
// CORS
CORS_ORIGINS: joi.string().default('*'),
// Email
SMTP_HOST: joi.string().required(),
SMTP_PORT: joi.number().default(587),
SMTP_USER: joi.string().required(),
SMTP_PASS: joi.string().required(),
EMAIL_FROM: joi.string().required(),
// File upload
UPLOAD_DIR: joi.string().default('uploads'),
MAX_FILE_SIZE: joi.number().default(10485760), // 10MB
// Logging
LOG_LEVEL: joi.string().valid('trace', 'debug', 'info', 'warn', 'error', 'fatal').default('info'),
// Rate limiting
RATE_LIMIT_WINDOW: joi.number().default(900000), // 15 minutes
RATE_LIMIT_MAX: joi.number().default(100)
}).unknown();
// Validate environment variables
const { error, value: envVars } = envSchema.validate(process.env);
if (error) {
throw new Error(\`Config validation error: \${error.message}\`);
}
export const config = {
env: envVars.NODE_ENV,
port: envVars.PORT,
host: envVars.HOST,
appName: envVars.APP_NAME,
version: envVars.VERSION,
database: {
url: envVars.DATABASE_URL,
pool: {
min: envVars.DB_POOL_MIN,
max: envVars.DB_POOL_MAX
}
},
redis: {
url: envVars.REDIS_URL
},
jwt: {
secret: envVars.JWT_SECRET,
expire: envVars.JWT_EXPIRE,
refreshExpire: envVars.JWT_REFRESH_EXPIRE
},
corsOrigins: envVars.CORS_ORIGINS.split(',').map(origin => origin.trim()),
email: {
host: envVars.SMTP_HOST,
port: envVars.SMTP_PORT,
user: envVars.SMTP_USER,
pass: envVars.SMTP_PASS,
from: envVars.EMAIL_FROM
},
upload: {
dir: envVars.UPLOAD_DIR,
maxFileSize: envVars.MAX_FILE_SIZE
},
logging: {
level: envVars.LOG_LEVEL
},
rateLimit: {
window: envVars.RATE_LIMIT_WINDOW,
max: envVars.RATE_LIMIT_MAX
}
};`,
// Routes setup
'src/routes/index.ts': `import * as restify from 'restify';
import { Router } from 'restify-router';
import authRoutes from './auth.routes';
import userRoutes from './user.routes';
import todoRoutes from './todo.routes';
import { authenticate } from '../middlewares/auth.middleware';
export function setupRoutes(server: restify.Server): void {
const router = new Router();
// API info endpoint
router.get('/api/v1', (req, res, next) => {
res.send({
message: '{{projectName}} API',
version: '1.0.0',
endpoints: {
auth: '/api/v1/auth',
users: '/api/v1/users',
todos: '/api/v1/todos',
docs: '/api-docs',
health: '/health',
metrics: '/metrics'
}
});
next();
});
// Mount routes
router.add('/api/v1/auth', authRoutes);
router.add('/api/v1/users', userRoutes);
router.add('/api/v1/todos', todoRoutes);
// Apply to server
router.applyRoutes(server);
// Metrics endpoint
server.get('/metrics', authenticate, (req, res, next) => {
const metrics = server.getMetrics();
res.send(metrics);
next();
});
}`,
// Authentication routes
'src/routes/auth.routes.ts': `import { Router } from 'restify-router';
import { AuthController } from '../controllers/auth.controller';
import { validateBody } from '../middlewares/validation.middleware';
import { authValidators } from '../validators/auth.validators';
import { authenticate } from '../middlewares/auth.middleware';
import { rateLimiter } from '../middlewares/rateLimit.middleware';
const router = new Router();
const authController = new AuthController();
// Register
router.post(
'/register',
rateLimiter('auth'),
validateBody(authValidators.register),
authController.register
);
// Login
router.post(
'/login',
rateLimiter('auth'),
validateBody(authValidators.login),
authController.login
);
// Refresh token
router.post(
'/refresh',
validateBody(authValidators.refreshToken),
authController.refreshToken
);
// Logout
router.post(
'/logout',
authenticate,
authController.logout
);
// Verify email
router.get(
'/verify/:token',
authController.verifyEmail
);
// Forgot password
router.post(
'/forgot-password',
rateLimiter('auth'),
validateBody(authValidators.forgotPassword),
authController.forgotPassword
);
// Reset password
router.post(
'/reset-password/:token',
validateBody(authValidators.resetPassword),
authController.resetPassword
);
export default router;`,
// User routes
'src/routes/user.routes.ts': `import { Router } from 'restify-router';
import { UserController } from '../controllers/user.controller';
import { authenticate, authorize } from '../middlewares/auth.middleware';
import { validateBody, validateParams, validateQuery } from '../middlewares/validation.middleware';
import { userValidators } from '../validators/user.validators';
import { upload } from '../middlewares/upload.middleware';
const router = new Router();
const userController = new UserController();
// All routes require authentication
router.use(authenticate);
// Get all users (admin only)
router.get(
'/',
authorize('admin'),
validateQuery(userValidators.getAllUsers),
userController.getAllUsers
);
// Get current user
router.get(
'/me',
userController.getCurrentUser
);
// Get user by ID
router.get(
'/:id',
validateParams(userValidators.getUserById),
userController.getUserById
);
// Update user
router.put(
'/:id',
validateParams(userValidators.getUserById),
validateBody(userValidators.updateUser),
userController.updateUser
);
// Delete user
router.del(
'/:id',
authorize('admin'),
validateParams(userValidators.getUserById),
userController.deleteUser
);
// Change password
router.post(
'/change-password',
validateBody(userValidators.changePassword),
userController.changePassword
);
// Upload avatar
router.post(
'/avatar',
upload.single('avatar'),
userController.uploadAvatar
);
export default router;`,
// Todo routes
'src/routes/todo.routes.ts': `import { Router } from 'restify-router';
import { TodoController } from '../controllers/todo.controller';
import { authenticate } from '../middlewares/auth.middleware';
import { validateBody, validateParams, validateQuery } from '../middlewares/validation.middleware';
import { todoValidators } from '../validators/todo.validators';
const router = new Router();
const todoController = new TodoController();
// All routes require authentication
router.use(authenticate);
// Get all todos with pagination and filtering
router.get(
'/',
validateQuery(todoValidators.getAllTodos),
todoController.getAllTodos
);
// Get todo by ID
router.get(
'/:id',
validateParams(todoValidators.getTodoById),
todoController.getTodoById
);
// Create todo
router.post(
'/',
validateBody(todoValidators.createTodo),
todoController.createTodo
);
// Update todo
router.put(
'/:id',
validateParams(todoValidators.getTodoById),
validateBody(todoValidators.updateTodo),
todoController.updateTodo
);
// Delete todo
router.del(
'/:id',
validateParams(todoValidators.getTodoById),
todoController.deleteTodo
);
// Bulk operations
router.post(
'/bulk/delete',
validateBody(todoValidators.bulkDelete),
todoController.bulkDelete
);
router.post(
'/bulk/update',
validateBody(todoValidators.bulkUpdate),
todoController.bulkUpdate
);
export default router;`,
// Authentication controller
'src/controllers/auth.controller.ts': `import * as restify from 'restify';
import { AuthService } from '../services/auth.service';
import { EmailService } from '../services/email.service';
import { logger } from '../utils/logger';
import { BadRequestError, UnauthorizedError } from 'restify-errors';
export class AuthController {
private authService: AuthService;
private emailService: EmailService;
constructor() {
this.authService = new AuthService();
this.emailService = new EmailService();
}
register = async (req: restify.Request, res: restify.Response, next: restify.Next) => {
try {
const { email, password, name } = req.body;
const result = await this.authService.register({ email, password, name });
// Send verification email
await this.emailService.sendVerificationEmail(email, result.verificationToken);
res.send(201, {
success: true,
message: 'Registration successful. Please check your email to verify your account.',
data: {
user: result.user,
accessToken: result.accessToken,
refreshToken: result.refreshToken
}
});
return next();
} catch (error) {
logger.error({ err: error }, 'Registration error');
return next(error);
}
};
login = async (req: restify.Request, res: restify.Response, next: restify.Next) => {
try {
const { email, password } = req.body;
const result = await this.authService.login(email, password);
// Set refresh token as HTTP-only cookie
res.setCookie('refreshToken', result.refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
});
res.send({
success: true,
message: 'Login successful',
data: {
user: result.user,
accessToken: result.accessToken
}
});
return next();
} catch (error) {
logger.error({ err: error }, 'Login error');
return next(error);
}
};
refreshToken = async (req: restify.Request, res: restify.Response, next: restify.Next) => {
try {
const refreshToken = req.cookies?.refreshToken || req.body.refreshToken;
if (!refreshToken) {
throw new UnauthorizedError('Refresh token not provided');
}
const result = await this.authService.refreshToken(refreshToken);
res.send({
success: true,
data: {
accessToken: result.accessToken
}
});
return next();
} catch (error) {
logger.error({ err: error }, 'Refresh token error');
return next(error);
}
};
logout = async (req: restify.Request, res: restify.Response, next: restify.Next) => {
try {
const userId = req.user?.id;
if (userId) {
await this.authService.logout(userId);
}
res.clearCookie('refreshToken');
res.send({
success: true,
message: 'Logout successful'
});
return next();
} catch (error) {
logger.error({ err: error }, 'Logout error');
return next(error);
}
};
verifyEmail = async (req: restify.Request, res: restify.Response, next: restify.Next) => {
try {
const { token } = req.params;
await this.authService.verifyEmail(token);
res.send({
success: true,
message: 'Email verified successfully'
});
return next();
} catch (error) {
logger.error({ err: error }, 'Email verification error');
return next(error);
}
};
forgotPassword = async (req: restify.Request, res: restify.Response, next: restify.Next) => {
try {
const { email } = req.body;
const resetToken = await this.authService.forgotPassword(email);
// Send reset email
await this.emailService.sendPasswordResetEmail(email, resetToken);
res.send({
success: true,
message: 'Password reset email sent'
});
return next();
} catch (error) {
logger.error({ err: error }, 'Forgot password error');
return next(error);
}
};
resetPassword = async (req: restify.Request, res: restify.Response, next: restify.Next) => {
try {
const { token } = req.params;
const { password } = req.body;
await this.authService.resetPassword(token, password);
res.send({
success: true,
message: 'Password reset successful'
});
return next();
} catch (error) {
logger.error({ err: error }, 'Reset password error');
return next(error);
}
};
}`,
// User controller
'src/controllers/user.controller.ts': `import * as restify from 'restify';
import { UserService } from '../services/user.service';
import { ForbiddenError, BadRequestError } from 'restify-errors';
import { logger } from '../utils/logger';
export class UserController {
private userService: UserService;
constructor() {
this.userService = new UserService();
}
getAllUsers = async (req: restify.Request, res: restify.Response, next: restify.Next) => {
try {
const { page = 1, limit = 10, search } = req.query;
const result = await this.userService.getAllUsers({
page: Number(page),
limit: Number(limit),
search: search as string
});
res.send({
success: true,
data: result
});
return next();
} catch (error) {
logger.error({ err: error }, 'Get all users error');
return next(error);
}
};
getCurrentUser = async (req: restify.Request, res: restify.Response, next: restify.Next) => {
try {
const userId = req.user!.id;
const user = await this.userService.getUserById(userId);
res.send({
success: true,
data: user
});
return next();
} catch (error) {
logger.error({ err: error }, 'Get current user error');
return next(error);
}
};
getUserById = async (req: restify.Request, res: restify.Response, next: restify.Next) => {
try {
const { id } = req.params;
const user = await this.userService.getUserById(id);
res.send({
success: true,
data: user
});
return next();
} catch (error) {
logger.error({ err: error }, 'Get user by ID error');
return next(error);
}
};
updateUser = async (req: restify.Request, res: restify.Response, next: restify.Next) => {
try {
const { id } = req.params;
const updates = req.body;
// Ensure users can only update their own profile unless admin
if (req.user!.id !== id && req.user!.role !== 'admin') {
throw new ForbiddenError('Forbidden');
}
const user = await this.userService.updateUser(id, updates);
res.send({
success: true,
message: 'User updated successfully',
data: user
});
return next();
} catch (error) {
logger.error({ err: error }, 'Update user error');
return next(error);
}
};
deleteUser = async (req: restify.Request, res: restify.Response, next: restify.Next) => {
try {
const { id } = req.params;
await this.userService.deleteUser(id);
res.send({
success: true,
message: 'User deleted successfully'
});
return next();
} catch (error) {
logger.error({ err: error }, 'Delete user error');
return next(error);
}
};
changePassword = async (req: restify.Request, res: restify.Response, next: restify.Next) => {
try {
const userId = req.user!.id;
const { currentPassword, newPassword } = req.body;
await this.userService.changePassword(userId, currentPassword, newPassword);
res.send({
success: true,
message: 'Password changed successfully'
});
return next();
} catch (error) {
logger.error({ err: error }, 'Change password error');
return next(error);
}
};
uploadAvatar = async (req: restify.Request, res: restify.Response, next: restify.Next) => {
try {
const userId = req.user!.id;
if (!req.file) {
throw new BadRequestError('No file uploaded');
}
const avatarUrl = await this.userService.updateAvatar(userId, req.file);
res.send({
success: true,
message: 'Avatar uploaded successfully',
data: { avatarUrl }
});
return next();
} catch (error) {
logger.error({ err: error }, 'Upload avatar error');
return next(error);
}
};
}`,
// Todo controller
'src/controllers/todo.controller.ts': `import * as restify from 'restify';
import { TodoService } from '../services/todo.service';
import { BadRequestError } from 'restify-errors';
import { logger } from '../utils/logger';
export class TodoController {
private todoService: TodoService;
constructor() {
this.todoService = new TodoService();
}
getAllTodos = async (req: restify.Request, res: restify.Response, next: restify.Next) => {
try {
const userId = req.user!.id;
const { page = 1, limit = 10, status, priority } = req.query;
const result = await this.todoService.getAllTodos({
userId,
page: Number(page),
limit: Number(limit),
status: status as string,
priority: priority as string
});
res.send({
success: true,
data: result
});
return next();
} catch (error) {
logger.error({ err: error }, 'Get all todos error');
return next(error);
}
};
getTodoById = async (req: restify.Request, res: restify.Response, next: restify.Next) => {
try {
const { id } = req.params;
const userId = req.user!.id;
const todo = await this.todoService.getTodoById(id, userId);
res.send({
success: true,
data: todo
});
return next();
} catch (error) {
logger.error({ err: error }, 'Get todo by ID error');
return next(error);
}
};
createTodo = async (req: restify.Request, res: restify.Response, next: restify.Next) => {
try {
const userId = req.user!.id;
const todoData = { ...req.body, userId };
const todo = await this.todoService.createTodo(todoData);
res.send(201, {
success: true,
message: 'Todo created successfully',
data: todo
});
return next();
} catch (error) {
logger.error({ err: error }, 'Create todo error');
return next(error);
}
};
updateTodo = async (req: restify.Request, res: restify.Response, next: restify.Next) => {
try {
const { id } = req.params;
const userId = req.user!.id;
const updates = req.body;
const todo = await this.todoService.updateTodo(id, userId, updates);
res.send({
success: true,
message: 'Todo updated successfully',
data: todo
});
return next();
} catch (error) {
logger.error({ err: error }, 'Update todo error');
return next(error);
}
};
deleteTodo = async (req: restify.Request, res: restify.Response, next: restify.Next) => {
try {
const { id } = req.params;
const userId = req.user!.id;
await this.todoService.deleteTodo(id, userId);
res.send({
success: true,
message: 'Todo deleted successfully'
});
return next();
} catch (error) {
logger.error({ err: error }, 'Delete todo error');
return next(error);
}
};
bulkDelete = async (req: restify.Request, res: restify.Response, next: restify.Next) => {
try {
const userId = req.user!.id;
const { ids } = req.body;
if (!Array.isArray(ids) || ids.length === 0) {
throw new BadRequestError('Invalid todo IDs');
}
const count = await this.todoService.bulkDelete(ids, userId);
res.send({
success: true,
message: \`\${count} todos deleted successfully\`
});
return next();
} catch (error) {
logger.error({ err: error }, 'Bulk delete error');
return next(error);
}
};
bulkUpdate = async (req: restify.Request, res: restify.Response, next: restify.Next) => {
try {
const userId = req.user!.id;
const { ids, updates } = req.body;
if (!Array.isArray(ids) || ids.length === 0) {
throw new BadRequestError('Invalid todo IDs');
}
const count = await this.todoService.bulkUpdate(ids, userId, updates);
res.send({
success: true,
message: \`\${count} todos updated successfully\`
});
return next();
} catch (error) {
logger.error({ err: error }, 'Bulk update error');
return next(error);
}
};
}`,
// Authentication middleware
'src/middlewares/auth.middleware.ts': `import * as restify from 'restify';
import * as jwt from 'jsonwebtoken';
import { UnauthorizedError, ForbiddenError } from 'restify-errors';
import { config } from '../config/config';
import { UserService } from '../services/user.service';
interface JwtPayload {
id: string;
email: string;
role: string;
}
declare module 'restify' {
interface Request {
user?: JwtPayload;
}
}
const userService = new UserService();
export const authenticate = async (
req: restify.Request,
res: restify.Response,
next: restify.Next
) => {
try {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new UnauthorizedError('No token provided');
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, config.jwt.secret) as JwtPayload;
// Check if user still exists
const user = await userService.getUserById(decoded.id);
if (!user) {
throw new UnauthorizedError('User no longer exists');
}
// Attach user to request
req.user = {
id: decoded.id,
email: decoded.email,
role: decoded.role
};
return next();
} catch (error) {
throw new UnauthorizedError('Invalid token');
}
} catch (error) {
return next(error);
}
};
export const authorize = (...roles: string[]) => {
return (req: restify.Request, res: restify.Response, next: restify.Next) => {
if (!req.user) {
return next(new UnauthorizedError('Not authenticated'));
}
if (!roles.includes(req.user.role)) {
return next(new ForbiddenError('Not authorized for this resource'));
}
return next();
};
};`,
// Error handling middleware
'src/middlewares/error.middleware.ts': `import * as restify from 'restify';
import { logger } from '../utils/logger';
export const errorHandler = (
req: restify.Request,
res: restify.Response,
err: any,
callback: Function
) => {
// Log the error
logger.error({
err,
req: {
method: req.method,
url: req.url,
headers: req.headers,
params: req.params,
query: req.query,
body: req.body
}
}, 'Request error');
// Handle known Restify errors
if (err.name === 'ValidationError') {
res.send(400, {
success: false,
error: {
code: 'VALIDATION_ERROR',
message: 'Validation failed',
details: err.body
}
});
return callback();
}
if (err.name === 'ResourceNotFoundError') {
res.send(404, {
success: false,
error: {
code: 'NOT_FOUND',
message: err.message || 'Resource not found'
}
});
return callback();
}
if (err.name === 'InvalidCredentialsError' || err.name === 'UnauthorizedError') {
res.send(401, {
success: false,
error: {
code: 'UNAUTHORIZED',
message: err.message || 'Unauthorized'
}
});
return callback();
}
if (err.name === 'ForbiddenError') {
res.send(403, {
success: false,
error: {
code: 'FORBIDDEN',
message: err.message || 'Forbidden'
}
});
return callback();
}
// Database errors
if (err.code === '23505') {
res.send(409, {
success: false,
error: {
code: 'DUPLICATE_ENTRY',
message: 'Duplicate entry'
}
});
return callback();
}
// Default error response
const statusCode = err.statusCode || 500;
const message = statusCode === 500 ? 'Internal server error' : err.message;
res.send(statusCode, {
success: false,
error: {
code: err.code || 'INTERNAL_ERROR',
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
}
});
return callback();
};`,
// Validation middleware
'src/middlewares/validation.middleware.ts': `import * as restify from 'restify';
import * as joi from 'joi';
import { BadRequestError } from 'restify-errors';
export const validateBody = (schema: joi.Schema) => {
return (req: restify.Request, res: restify.Response, next: restify.Next) => {
const { error, value } = schema.validate(req.body, { abortEarly: false });
if (error) {
const details = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}));
return next(new BadRequestError({
message: 'Validation failed',
body: details
}));
}
req.body = value;
return next();
};
};
export const validateParams = (schema: joi.Schema) => {
return (req: restify.Request, res: restify.Response, next: restify.Next) => {
const { error, value } = schema.validate(req.params, { abortEarly: false });
if (error) {
const details = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}));
return next(new BadRequestError({
message: 'Validation failed',
body: details
}));
}
req.params = value;
return next();
};
};
export const validateQuery = (schema: joi.Schema) => {
return (req: restify.Request, res: restify.Response, next: restify.Next) => {
const { error, value } = schema.validate(req.query, { abortEarly: false });
if (error) {
const details = error.details.map(detail => ({
field: detail.path.join('.'),
message: detail.message
}));
return next(new BadRequestError({
message: 'Validation failed',
body: details
}));
}
req.query = value;
return next();
};
};`,
// Rate limiting middleware
'src/middlewares/rateLimit.middleware.ts': `import * as restify from 'restify';
import { redisClient } from '../config/redis';
import { TooManyRequestsError } from 'restify-errors';
import { config } from '../config/config';
interface RateLimitOptions {
window?: number;
max?: number;
keyPrefix?: string;
}
export const rateLimiter = (namespace: string, options: RateLimitOptions = {}) => {
const {
window = config.rateLimit.window,
max = config.rateLimit.max,
keyPrefix = 'rate_limit'
} = options;
return async (req: restify.Request, res: restify.Response, next: restify.Next) => {
const ip = req.connection.remoteAddress || 'unknown';
const key = \`\${keyPrefix}:\${namespace}:\${ip}\`;
try {
const current = await redisClient.incr(key);
if (current === 1) {
await redisClient.expire(key, Math.ceil(window / 1000));
}
const ttl = await redisClient.ttl(key);
res.header('X-RateLimit-Limit', max.toString());
res.header('X-RateLimit-Remaining', Math.max(0, max - current).toString());
res.header('X-RateLimit-Reset', new Date(Date.now() + ttl * 1000).toISOString());
if (current > max) {
return next(new TooManyRequestsError('Rate limit exceeded'));
}
return next();
} catch (error) {
// If Redis is down, allow the request
return next();
}
};
};`,
// Logger middleware
'src/middlewares/logger.middleware.ts': `import * as restify from 'restify';
import { logger } from '../utils/logger';
import { v4 as uuidv4 } from 'uuid';
export const requestLogger = (req: restify.Request, res: restify.Response, next: restify.Next) => {
const requestId = uuidv4();
const startTime = Date.now();
// Attach request ID
req.id = () => requestId;
res.header('X-Request-ID', requestId);
// Log request
logger.info({
requestId,
method: req.method,
url: req.url,
headers: req.headers,
query: req.query,
ip: req.connection.remoteAddress
}, 'Incoming request');
// Log response
res.on('finish', () => {
const duration = Date.now() - startTime;
logger.info({
requestId,
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration,
contentLength: res.getHeader('Content-Length')
}, 'Request completed');
});
return next();
};`,
// Upload middleware
'src/middlewares/upload.middleware.ts': `import * as multer from 'multer';
import * as path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { config } from '../config/config';
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, config.upload.dir);
},
filename: (req, file, cb) => {
const uniqueName = \`\${uuidv4()}\${path.extname(file.originalname)}\`;
cb(null, uniqueName);
}
});
const fileFilter = (req: any, file: Express.Multer.File, cb: multer.FileFilterCallback) => {
const allowedMimes = [
'image/jpeg',
'image/jpg',
'image/png',
'image/gif',
'image/webp'
];
if (allowedMimes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type. Only JPEG, PNG, GIF and WebP are allowed'));
}
};
export const upload = multer({
storage,
fileFilter,
limits: {
fileSize: config.upload.maxFileSize
}
});`,
// Auth validators
'src/validators/auth.validators.ts': `import * as joi from 'joi';
export const authValidators = {
register: joi.object({
email: joi.string().email().required(),
password: joi.string().min(8).required(),
name: joi.string().min(2).max(50).required()
}),
login: joi.object({
email: joi.string().email().required(),
password: joi.string().required()
}),
refreshToken: joi.object({
refreshToken: joi.string().optional()
}),
forgotPassword: joi.object({
email: joi.string().email().required()
}),
resetPassword: joi.object({
password: joi.string().min(8).required()
})
};`,
// User validators
'src/validators/user.validators.ts': `import * as joi from 'joi';
export const userValidators = {
getAllUsers: joi.object({
page: joi.number().integer().min(1).optional(),
limit: joi.number().integer().min(1).max(100).optional(),
search: joi.string().optional()
}),
getUserById: joi.object({
id: joi.string().uuid().required()
}),
updateUser: joi.object({
email: joi.string().email().optional(),
name: joi.string().min(2).max(50).optional()
}),
changePassword: joi.object({
currentPassword: joi.string().required(),
newPassword: joi.string().min(8).required()
})
};`,
// Todo validators
'src/validators/todo.validators.ts': `import * as joi from 'joi';
export const todoValidators = {
getAllTodos: joi.object({
page: joi.number().integer().min(1).optional(),
limit: joi.number().integer().min(1).max(100).optional(),
status: joi.string().valid('pending', 'in_progress', 'completed').optional(),
priority: joi.string().valid('low', 'medium', 'high').optional()
}),
getTodoById: joi.object({
id: joi.string().uuid().required()
}),
createTodo: joi.object({
title: joi.string().min(1).max(255).required(),
description: joi.string().max(1000).optional(),
priority: joi.string().valid('low', 'medium', 'high').optional(),
dueDate: joi.date().iso().optional()
}),
updateTodo: joi.object({
title: joi.string().min(1).max(255).optional(),
description: joi.string().max(1000).optional(),
status: joi.string().valid('pending', 'in_progress', 'completed').optional(),
priority: joi.string().valid('low', 'medium', 'high').optional(),
dueDate: joi.date().iso().optional()
}),
bulkDelete: joi.object({
ids: joi.array().items(joi.string().uuid()).min(1).required()
}),
bulkUpdate: joi.object({
ids: joi.array().items(joi.string().uuid()).min(1).required(),
updates: joi.object({
status: joi.string().valid('pending', 'in_progress', 'completed').optional(),
priority: joi.string().valid('low', 'medium', 'high').optional()
}).required()
})
};`,
// Database configuration
'src/config/database.ts': `import { Pool } from 'pg';
import { config } from './config';
import { logger } from '../utils/logger';
export const pool = new Pool({
connectionString: config.database.url,
min: config.database.pool.min,
max: config.database.pool.max,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
pool.on('error', (err) => {
logger.error('Unexpected database error on idle client', err);
});
export const connectDatabase = async (): Promise<void> => {
try {
const client = await pool.connect();
await client.query('SELECT NOW()');
client.release();
logger.info('Database connected successfully');
} catch (error) {
logger.error('Database connection failed:', error);
throw error;
}
};
export const query = async (text: string, params?: any[]) => {
const start = Date.now();
try {
const res = await pool.query(text, params);
const duration = Date.now() - start;
logger.debug({ text, duration, rows: res.rowCount }, 'Database query executed');
return res;
} catch (error) {
logger.error({ text, error }, 'Database query error');
throw error;
}
};
export const getClient = async () => {
const client = await pool.connect();
const query = client.query.bind(client);
const release = () => {
client.release();
};
// Set a timeout of 5 seconds, after which we will log this client's last query
const timeout = setTimeout(() => {
logger.error('A client has been checked out for more than 5 seconds!');
}, 5000);
const releaseWithTimeout = () => {
clearTimeout(timeout);
client.release();
};
return {
query,
release: releaseWithTimeout,
client
};
};`,
// Redis configuration
'src/config/redis.ts': `import { createClient } from 'redis';
import { config } from './config';
import { logger } from '../utils/logger';
export const redisClient = createClient({
url: config.redis.url,
socket: {
reconnectStrategy: (retries) => {
if (retries > 10) {
logger.error('Redis: Maximum reconnection attempts reached');
return new Error('Maximum reconnection attempts reached');
}
const delay = Math.min(retries * 100, 3000);
logger.info(\`Redis: Reconnecting in \${delay}ms...\`);
return delay;
}
}
});
redisClient.on('error', (err) => {
logger.error('Redis Client Error:', err);
});
redisClient.on('connect', () => {
logger.info('Redis Client Connected');
});
redisClient.on('ready', () => {
logger.info('Redis Client Ready');
});
redisClient.on('reconnecting', () => {
logger.warn('Redis Client Reconnecting');
});`,
// WebSocket configuration
'src/config/websocket.ts': `import { Server } from 'socket.io';
import * as jwt from 'jsonwebtoken';
import { config } from './config';
import { logger } from '../utils/logger';
interface SocketWithAuth extends Socket {
userId?: string;
}
export const initializeWebSocket = (server: any) => {
const io = new Server(server, {
cors: {
origin: config.corsOrigins,
credentials: true
}
});
// Authentication middleware
io.use(async (socket: any, next) => {
try {
const token = socket.handshake.auth.token;
if (!token) {
return next(new Error('Authentication error'));
}
const decoded = jwt.verify(token, config.jwt.secret) as any;
socket.userId = decoded.id;
next();
} catch (err) {
next(new Error('Authentication error'));
}
});
io.on('connection', (socket: any) => {
logger.info(\`User \${socket.userId} connected via WebSocket\`);
// Join user's personal room
if (socket.userId) {
socket.join(\`user:\${socket.userId}\`);
}
// Handle real-time events
socket.on('join-room', (roomId: string) => {
socket.join(roomId);
logger.info(\`User \${socket.userId} joined room \${roomId}\`);
});
socket.on('leave-room', (roomId: string) => {
socket.leave(roomId);
logger.info(\`User \${socket.userId} left room \${roomId}\`);
});
socket.on('todo-update', (data: any) => {
// Broadcast to all users in the room
socket.to(\`user:\${socket.userId}\`).emit('todo-updated', data);
});
socket.on('disconnect', () => {
logger.info(\`User \${socket.userId} disconnected\`);
});
});
return io;
};
export const emitToUser = (io: Server, userId: string, event: string, data: any) => {
io.to(\`user:\${userId}\`).emit(event, data);
};`,
// Swagger configuration
'src/config/swagger.ts': `import * as restify from 'restify';
import * as swaggerJsdoc from 'swagger-jsdoc';
import { config } from './config';
const options = {
definition: {
openapi: '3.0.0',
info: {
title: '{{projectName}} API',
version: config.version,
description: 'Restify REST API with TypeScript',
license: {
name: 'MIT',
url: 'https://spdx.org/licenses/MIT.html',
},
contact: {
name: 'API Support',
email: 'support@example.com',
},
},
servers: [
{
url: \`http://localhost:\${config.port}/api/v1\`,
description: 'Development server',
},
{
url: process.env.API_URL || 'https://api.example.com/v1',
description: 'Production server',
},
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
},
security: [
{
bearerAuth: [],
},
],
},
apis: ['./src/routes/*.ts', './src/models/*.ts'],
};
export function setupSwagger(server: restify.Server): void {
const specs = swaggerJsdoc(options);
server.get('/api-docs', (req, res, next) => {
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
res.end(\`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{projectName}} API Documentation</title>
<link rel="stylesheet" href="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui.css" />
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@4.5.0/swagger-ui-standalone-preset.js"></script>
<script>
window.onload = function() {
window.ui = SwaggerUIBundle({
url: '/api-docs.json',
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
};
</script>
</body>
</html>
\`);
next();
});
server.get('/api-docs.json', (req, res, next) => {
res.json(specs);
next();
});
}`,
// Logger utility
'src/utils/logger.ts': `import * as bunyan from 'bunyan';
import * as path from 'path';
import { config } from '../config/config';
const logDir = process.env.LOG_DIR || 'logs';
export const logger = bunyan.createLogger({
name: config.appName,
level: config.logging.level as any,
serializers: bunyan.stdSerializers,
streams: [
{
level: 'info',
stream: process.stdout
},
{
level: 'error',
path: path.join(logDir, 'error.log')
},
{
level: 'info',
path: path.join(logDir, 'combined.log')
}
]
});`,
// Graceful shutdown utility
'src/utils/gracefulShutdown.ts': `import * as restify from 'restify';
import { logger } from './logger';
import { pool } from '../config/database';
export const gracefulShutdown = async (server: restify.Server, redisClient: any) => {
logger.info('Received shutdown signal, starting graceful shutdown...');
// Stop accepting new connections
server.close(() => {
logger.info('HTTP server closed');
});
// Close database connections
try {
await pool.end();
logger.info('Database connections closed');
} catch (error) {
logger.error('Error closing database connections:', error);
}
// Close Redis connection
try {
await redisClient.quit();
logger.info('Redis connection closed');
} catch (error) {
logger.error('Error closing Redis connection:', error);
}
// Force exit after 10 seconds
setTimeout(() => {
logger.error('Could not close connections in time, forcefully shutting down');