feat: add comprehensive NodeJS development skills and rules
Based on Planner and Memory Manager analysis: New Skills (8): - nodejs-express-patterns: App structure, routing, middleware - nodejs-security-owasp: OWASP Top 10 security practices - nodejs-testing-jest: Unit/integration tests, mocking - nodejs-auth-jwt: JWT authentication, OAuth, sessions - nodejs-error-handling: Error classes, middleware, async handlers - nodejs-middleware-patterns: Auth, validation, rate limiting - nodejs-db-patterns: SQLite, PostgreSQL, MongoDB patterns - nodejs-npm-management: package.json, scripts, dependencies New Rules: - nodejs.md: Code style, security, best practices Updated: - backend-developer.md: Added skills reference table Milestone: #48 NodeJS Development Coverage Related: Planner & Memory Manager analysis results
This commit is contained in:
341
.kilo/skills/nodejs-error-handling/SKILL.md
Normal file
341
.kilo/skills/nodejs-error-handling/SKILL.md
Normal file
@@ -0,0 +1,341 @@
|
||||
# NodeJS Error Handling
|
||||
|
||||
Comprehensive error handling patterns for Node.js applications.
|
||||
|
||||
## Error Classes
|
||||
|
||||
```javascript
|
||||
// src/utils/AppError.js
|
||||
class AppError extends Error {
|
||||
constructor(message, statusCode) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
|
||||
this.isOperational = true;
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
class NotFoundError extends AppError {
|
||||
constructor(resource = 'Resource') {
|
||||
super(`${resource} not found`, 404);
|
||||
}
|
||||
}
|
||||
|
||||
class ValidationError extends AppError {
|
||||
constructor(errors) {
|
||||
super('Validation failed', 400);
|
||||
this.errors = errors;
|
||||
}
|
||||
}
|
||||
|
||||
class UnauthorizedError extends AppError {
|
||||
constructor(message = 'Unauthorized') {
|
||||
super(message, 401);
|
||||
}
|
||||
}
|
||||
|
||||
class ForbiddenError extends AppError {
|
||||
constructor(message = 'Forbidden') {
|
||||
super(message, 403);
|
||||
}
|
||||
}
|
||||
|
||||
class ConflictError extends AppError {
|
||||
constructor(message = 'Conflict') {
|
||||
super(message, 409);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
AppError,
|
||||
NotFoundError,
|
||||
ValidationError,
|
||||
UnauthorizedError,
|
||||
ForbiddenError,
|
||||
ConflictError
|
||||
};
|
||||
```
|
||||
|
||||
## Error Middleware
|
||||
|
||||
```javascript
|
||||
// src/middleware/error.js
|
||||
const AppError = require('../utils/AppError');
|
||||
|
||||
// Not found handler
|
||||
function notFoundHandler(req, res, next) {
|
||||
next(new AppError(`Cannot find ${req.originalUrl} on this server`, 404));
|
||||
}
|
||||
|
||||
// Global error handler
|
||||
function errorHandler(err, req, res, next) {
|
||||
err.statusCode = err.statusCode || 500;
|
||||
err.status = err.status || 'error';
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
sendErrorDev(err, res);
|
||||
} else {
|
||||
sendErrorProd(err, res);
|
||||
}
|
||||
}
|
||||
|
||||
function sendErrorDev(err, res) {
|
||||
res.status(err.statusCode).json({
|
||||
status: err.status,
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
error: err
|
||||
});
|
||||
}
|
||||
|
||||
function sendErrorProd(err, res) {
|
||||
// Operational error: send message to client
|
||||
if (err.isOperational) {
|
||||
res.status(err.statusCode).json({
|
||||
status: err.status,
|
||||
message: err.message
|
||||
});
|
||||
} else {
|
||||
// Programming error: don't leak details
|
||||
console.error('ERROR:', err);
|
||||
res.status(500).json({
|
||||
status: 'error',
|
||||
message: 'Something went wrong'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { notFoundHandler, errorHandler };
|
||||
```
|
||||
|
||||
## Async Error Wrapper
|
||||
|
||||
```javascript
|
||||
// src/middleware/asyncHandler.js
|
||||
function asyncHandler(fn) {
|
||||
return (req, res, next) => {
|
||||
Promise.resolve(fn(req, res, next)).catch(next);
|
||||
};
|
||||
}
|
||||
|
||||
// Usage
|
||||
router.get('/users/:id', asyncHandler(controller.getById));
|
||||
|
||||
// Or wrap entire controller
|
||||
const wrapController = (controller) => {
|
||||
const wrapped = {};
|
||||
for (const [key, fn] of Object.entries(controller)) {
|
||||
wrapped[key] = asyncHandler(fn.bind(controller));
|
||||
}
|
||||
return wrapped;
|
||||
};
|
||||
|
||||
module.exports = { asyncHandler, wrapController };
|
||||
```
|
||||
|
||||
## Validation Errors
|
||||
|
||||
```javascript
|
||||
// src/middleware/validation.js
|
||||
const { validationResult } = require('express-validator');
|
||||
const { ValidationError } = require('../utils/AppError');
|
||||
|
||||
function validate(validations) {
|
||||
return async (req, res, next) => {
|
||||
await Promise.all(validations.map(validation => validation.run(req)));
|
||||
|
||||
const errors = validationResult(req);
|
||||
|
||||
if (errors.isEmpty()) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const extractedErrors = errors.array().map(err => ({
|
||||
field: err.path,
|
||||
message: err.msg,
|
||||
value: err.value
|
||||
}));
|
||||
|
||||
return next(new ValidationError(extractedErrors));
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { validate };
|
||||
```
|
||||
|
||||
## Database Error Handling
|
||||
|
||||
```javascript
|
||||
// src/middleware/dbErrorHandler.js
|
||||
const AppError = require('../utils/AppError');
|
||||
|
||||
function dbErrorHandler(err, req, res, next) {
|
||||
// PostgreSQL unique violation
|
||||
if (err.code === '23505') {
|
||||
const field = err.constraint.split('_')[0];
|
||||
return next(new AppError(`${field} already exists`, 409));
|
||||
}
|
||||
|
||||
// PostgreSQL foreign key violation
|
||||
if (err.code === '23503') {
|
||||
return next(new AppError('Referenced resource not found', 400));
|
||||
}
|
||||
|
||||
// SQLite unique violation
|
||||
if (err.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
||||
return next(new AppError('Resource already exists', 409));
|
||||
}
|
||||
|
||||
// MongoDB duplicate key
|
||||
if (err.code === 11000) {
|
||||
const field = Object.keys(err.keyValue)[0];
|
||||
return next(new AppError(`${field} already exists`, 409));
|
||||
}
|
||||
|
||||
// MongoDB validation error
|
||||
if (err.name === 'ValidationError') {
|
||||
const messages = Object.values(err.errors).map(e => e.message);
|
||||
return next(new AppError(messages.join('. '), 400));
|
||||
}
|
||||
|
||||
// MongoDB cast error
|
||||
if (err.name === 'CastError') {
|
||||
return next(new AppError('Invalid ID format', 400));
|
||||
}
|
||||
|
||||
next(err);
|
||||
}
|
||||
|
||||
module.exports = dbErrorHandler;
|
||||
```
|
||||
|
||||
## Logging Errors
|
||||
|
||||
```javascript
|
||||
// src/utils/logger.js
|
||||
const pino = require('pino');
|
||||
|
||||
const logger = pino({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
transport: process.env.NODE_ENV === 'development'
|
||||
? { target: 'pino-pretty' }
|
||||
: undefined
|
||||
});
|
||||
|
||||
function logError(err, req = null) {
|
||||
logger.error({
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
statusCode: err.statusCode || 500,
|
||||
path: req?.path,
|
||||
method: req?.method,
|
||||
user: req?.user?.id,
|
||||
body: req?.body,
|
||||
query: req?.query,
|
||||
params: req?.params,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
function logInfo(message, data = {}) {
|
||||
logger.info({ message, ...data });
|
||||
}
|
||||
|
||||
module.exports = { logger, logError, logInfo };
|
||||
```
|
||||
|
||||
## Unhandled Rejections
|
||||
|
||||
```javascript
|
||||
// src/server.js
|
||||
process.on('unhandledRejection', (err) => {
|
||||
console.error('UNHANDLED REJECTION:', err);
|
||||
logError(err);
|
||||
|
||||
// Graceful shutdown
|
||||
server.close(() => {
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('UNCAUGHT EXCEPTION:', err);
|
||||
logError(err);
|
||||
|
||||
// Must exit immediately for uncaught exceptions
|
||||
process.exit(1);
|
||||
});
|
||||
```
|
||||
|
||||
## API Error Responses
|
||||
|
||||
```javascript
|
||||
// Standardized error responses
|
||||
const errorResponses = {
|
||||
400: {
|
||||
status: 'fail',
|
||||
message: 'Bad Request',
|
||||
errors: []
|
||||
},
|
||||
401: {
|
||||
status: 'fail',
|
||||
message: 'Unauthorized'
|
||||
},
|
||||
403: {
|
||||
status: 'fail',
|
||||
message: 'Forbidden'
|
||||
},
|
||||
404: {
|
||||
status: 'fail',
|
||||
message: 'Not Found'
|
||||
},
|
||||
409: {
|
||||
status: 'fail',
|
||||
message: 'Conflict'
|
||||
},
|
||||
500: {
|
||||
status: 'error',
|
||||
message: 'Internal Server Error'
|
||||
}
|
||||
};
|
||||
|
||||
// Example responses
|
||||
{
|
||||
"status": "fail",
|
||||
"message": "Validation failed",
|
||||
"errors": [
|
||||
{ "field": "email", "message": "Invalid email format" },
|
||||
{ "field": "password", "message": "Must be at least 8 characters" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling in Tests
|
||||
|
||||
```javascript
|
||||
// tests/unit/errors.test.js
|
||||
describe('Error Handling', () => {
|
||||
it('should handle AppError', () => {
|
||||
const error = new AppError('Test error', 400);
|
||||
|
||||
expect(error.statusCode).toBe(400);
|
||||
expect(error.status).toBe('fail');
|
||||
expect(error.isOperational).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle async errors', async () => {
|
||||
const req = {};
|
||||
const res = {};
|
||||
const next = jest.fn();
|
||||
|
||||
const handler = asyncHandler(async () => {
|
||||
throw new AppError('Test', 400);
|
||||
});
|
||||
|
||||
await handler(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(expect.any(AppError));
|
||||
});
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user