nodejs-typescript-app
2
总安装量
2
周安装量
#69232
全站排名
安装命令
npx skills add https://github.com/vikashvikram/agent-skills --skill nodejs-typescript-app
Agent 安装分布
trae
2
gemini-cli
2
claude-code
2
codex
2
kiro-cli
2
cursor
2
Skill 文档
Node.js TypeScript Application
Project Structure
backend/
âââ routes/ # Express route handlers
â âââ index.ts # Route aggregator
â âââ upload.ts
â âââ transform.ts
â âââ datasets.ts
â âââ workflows.ts
âââ services/ # Business logic layer
â âââ dataService.ts
â âââ transformService.ts
â âââ profileService.ts
âââ utils/ # Shared utilities
â âââ dbHelpers.ts # Database operations
â âââ queryBuilder.ts # SQL query construction
â âââ logger.ts # Logging utility
âââ types/ # TypeScript definitions
â âââ index.ts
âââ middleware/ # Express middleware
â âââ errorHandler.ts
âââ index.ts # Application entry point
âââ package.json
âââ tsconfig.json
TypeScript Configuration
Essential tsconfig.json settings:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["./**/*.ts"],
"exclude": ["node_modules", "dist"]
}
Package.json Scripts
{
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "nodemon --exec ts-node index.ts",
"lint": "eslint . --ext .ts",
"test": "jest"
}
}
Express Application Setup
index.ts – Application entry point:
import express, { Request, Response, NextFunction } from 'express';
import cors from 'cors';
import path from 'path';
import routes from './routes';
import { errorHandler } from './middleware/errorHandler';
import { logger } from './utils/logger';
const app = express();
const PORT = process.env.PORT || 3001;
// Middleware
app.use(cors());
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
// Static files (for serving frontend build)
app.use(express.static(path.join(__dirname, '../public')));
// API routes
app.use('/api', routes);
// Error handling middleware (must be last)
app.use(errorHandler);
// Start server
app.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
});
export default app;
Route Handlers Pattern
routes/index.ts – Route aggregator:
import { Router } from 'express';
import uploadRoutes from './upload';
import transformRoutes from './transform';
import datasetRoutes from './datasets';
import workflowRoutes from './workflows';
const router = Router();
router.use('/upload', uploadRoutes);
router.use('/transform', transformRoutes);
router.use('/datasets', datasetRoutes);
router.use('/workflows', workflowRoutes);
export default router;
routes/datasets.ts – Feature route file:
import { Router, Request, Response, NextFunction } from 'express';
import { datasetService } from '../services/datasetService';
import { logger } from '../utils/logger';
const router = Router();
// Wrap async handlers to catch errors
const asyncHandler = (fn: Function) => (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
router.get('/', asyncHandler(async (req: Request, res: Response) => {
const datasets = await datasetService.getAll();
res.json(datasets);
}));
router.post('/save', asyncHandler(async (req: Request, res: Response) => {
const { datasetName, transformations } = req.body;
if (!datasetName?.trim()) {
return res.status(400).json({ error: 'Dataset name is required' });
}
await datasetService.save(datasetName, transformations);
res.json({ message: 'Dataset saved successfully' });
}));
router.delete('/:fileName', asyncHandler(async (req: Request, res: Response) => {
const { fileName } = req.params;
await datasetService.delete(fileName);
res.json({ message: 'Dataset deleted successfully' });
}));
export default router;
Service Layer Pattern
Services contain business logic, separate from HTTP concerns:
// services/datasetService.ts
import { db } from '../utils/dbHelpers';
import { queryBuilder } from '../utils/queryBuilder';
import type { Dataset, Transformation } from '../types';
export const datasetService = {
async getAll(): Promise<Dataset[]> {
// Business logic here
return db.query('SELECT * FROM datasets ORDER BY created_at DESC');
},
async save(name: string, transformations: Transformation[]): Promise<void> {
const query = queryBuilder.buildTransformQuery(transformations);
await db.execute(query);
// Save metadata
},
async delete(fileName: string): Promise<void> {
await db.execute('DELETE FROM datasets WHERE file_name = ?', [fileName]);
},
};
Error Handling Middleware
middleware/errorHandler.ts:
import { Request, Response, NextFunction } from 'express';
import { logger } from '../utils/logger';
export class AppError extends Error {
constructor(
message: string,
public statusCode: number = 500,
public isOperational: boolean = true
) {
super(message);
this.name = 'AppError';
Error.captureStackTrace(this, this.constructor);
}
}
export const errorHandler = (
err: Error | AppError,
req: Request,
res: Response,
next: NextFunction
): void => {
const statusCode = err instanceof AppError ? err.statusCode : 500;
const message = err.message || 'Internal server error';
logger.error(`${req.method} ${req.path} - ${message}`, {
stack: err.stack,
statusCode,
});
res.status(statusCode).json({
error: message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
};
Logger Utility
utils/logger.ts:
import winston from 'winston';
const logFormat = winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.printf(({ level, message, timestamp, stack }) => {
return `${timestamp} [${level.toUpperCase()}]: ${message}${stack ? `\n${stack}` : ''}`;
})
);
export const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: logFormat,
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'server.log' }),
],
});
Type Definitions
types/index.ts:
export interface Dataset {
name: string;
fileName: string;
createdAt: string;
rowCount: number;
columnCount: number;
}
export interface Transformation {
id: string;
name: string;
params: Record<string, unknown>;
}
export interface TransformResult {
success: boolean;
rowCount: number;
columns: string[];
}
// Express request extensions
export interface TypedRequest<T = unknown> extends Express.Request {
body: T;
}
Database Helpers Pattern
utils/dbHelpers.ts (example with DuckDB):
import * as duckdb from 'duckdb';
import { logger } from './logger';
let db: duckdb.Database | null = null;
let connection: duckdb.Connection | null = null;
export const initDatabase = (): void => {
db = new duckdb.Database(':memory:');
connection = db.connect();
logger.info('Database initialized');
};
export const query = async <T>(sql: string, params?: unknown[]): Promise<T[]> => {
if (!connection) throw new Error('Database not initialized');
return new Promise((resolve, reject) => {
connection!.all(sql, params || [], (err, result) => {
if (err) reject(err);
else resolve(result as T[]);
});
});
};
export const execute = async (sql: string, params?: unknown[]): Promise<void> => {
if (!connection) throw new Error('Database not initialized');
return new Promise((resolve, reject) => {
connection!.run(sql, params || [], (err) => {
if (err) reject(err);
else resolve();
});
});
};
export const closeDatabase = (): void => {
if (connection) connection.close();
if (db) db.close();
logger.info('Database closed');
};
Async Handler Pattern
Wrap all async route handlers to catch errors:
// Option 1: Utility function
const asyncHandler = (fn: Function) =>
(req: Request, res: Response, next: NextFunction) =>
Promise.resolve(fn(req, res, next)).catch(next);
// Option 2: express-async-handler package
import asyncHandler from 'express-async-handler';
router.get('/', asyncHandler(async (req, res) => {
const data = await service.getData();
res.json(data);
}));
Input Validation
router.post('/save', asyncHandler(async (req: Request, res: Response) => {
const { name, data } = req.body;
// Validate required fields
if (!name?.trim()) {
return res.status(400).json({ error: 'Name is required' });
}
if (!Array.isArray(data)) {
return res.status(400).json({ error: 'Data must be an array' });
}
// Proceed with valid input
await service.save(name.trim(), data);
res.json({ message: 'Saved successfully' });
}));
File Upload Handling
import multer from 'multer';
import path from 'path';
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, process.env.UPLOAD_DIR || './uploads');
},
filename: (req, file, cb) => {
const uniqueSuffix = `${Date.now()}-${Math.round(Math.random() * 1E9)}`;
cb(null, `${uniqueSuffix}${path.extname(file.originalname)}`);
},
});
const upload = multer({
storage,
limits: { fileSize: 100 * 1024 * 1024 }, // 100MB
fileFilter: (req, file, cb) => {
const allowedTypes = ['.csv', '.xlsx', '.parquet'];
const ext = path.extname(file.originalname).toLowerCase();
if (allowedTypes.includes(ext)) {
cb(null, true);
} else {
cb(new Error(`File type ${ext} not allowed`));
}
},
});
router.post('/', upload.single('file'), asyncHandler(async (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
// Process file...
}));
Environment Variables
// Load at app start
import dotenv from 'dotenv';
dotenv.config();
// Access with defaults
const config = {
port: parseInt(process.env.PORT || '3001', 10),
nodeEnv: process.env.NODE_ENV || 'development',
logLevel: process.env.LOG_LEVEL || 'info',
uploadDir: process.env.UPLOAD_DIR || './uploads',
dataDir: process.env.DATA_DIR || './data',
};
export default config;
Dependencies
Essential packages:
{
"dependencies": {
"express": "^4.x",
"cors": "^2.x",
"dotenv": "^16.x",
"multer": "^1.x",
"winston": "^3.x"
},
"devDependencies": {
"typescript": "^5.x",
"@types/node": "^20.x",
"@types/express": "^4.x",
"@types/cors": "^2.x",
"@types/multer": "^1.x",
"nodemon": "^3.x",
"ts-node": "^10.x",
"jest": "^29.x",
"@types/jest": "^29.x",
"ts-jest": "^29.x"
}
}
Testing Pattern
tests/routes/datasets.test.ts:
import request from 'supertest';
import app from '../../index';
import { datasetService } from '../../services/datasetService';
jest.mock('../../services/datasetService');
const mockedService = datasetService as jest.Mocked<typeof datasetService>;
describe('GET /api/datasets', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return all datasets', async () => {
const mockDatasets = [{ name: 'test', fileName: 'test.csv' }];
mockedService.getAll.mockResolvedValue(mockDatasets);
const response = await request(app).get('/api/datasets');
expect(response.status).toBe(200);
expect(response.body).toEqual(mockDatasets);
});
});