A comprehensive guide to backend development best practices, common pitfalls, and essential guidelines for building robust, scalable, and maintainable applications.
Quick Read Alert: This is a condensed overview of our comprehensive backend best practices. For detailed examples, complete code snippets, and in-depth explanations, read the full documentation here.
Coding Best Practices
1. Naming Conventions
Good Practices:
- Use descriptive names that convey intent
- Follow camelCase for variables/functions, PascalCase for classes
- Avoid abbreviations unless widely accepted (
userIdinstead ofuid) - Ensure names are self-explanatory (
isUserActiveinstead offlag1)
// β
Good: Clear and descriptive
const dynamicFieldsIsValid = templateHelper.verifyDynamicFields(
schemaValidationData?.body,
schemaValidationData?.dynamic_fields
)
// β Bad: Unclear and non-descriptive
const flag1 = helper.check(data?.body, data?.fields)
2. Code Commenting
Focus on explaining βwhyβ rather than βhowβ:
- Clarify tricky logic and algorithm choices
- Document API references and external dependencies
- Explain business logic and assumptions
- Use tools like JSDoc or Mintlify
/* Maximum 6 decimal places are rounded off, to avoid long decimal
numbers and prevent partial payments */
amount = helper.roundToSixDecimalPlaces(exchangeRateData.rate * existingOrder.order_amount)
/* Considering a Minimum Payable amount for crypto payments:
This avoids partial payments due to exchange rate variations
Client suggested 0.5% acceptance parameter for incomplete sales */
3. Code Optimization
Key Areas:
- Avoid unnecessary computations in loops
- Use helper functions for modularity
- Optimize database queries (avoid
SELECT *) - Implement caching mechanisms (Redis, in-memory)
- Use batch processing for bulk operations
Error Handling:
// β
Proper error handling with try-catch
async function getUserProfile(userId) {
try {
const user = await db.getUserById(userId);
if (!user) {
return { statusCode: 404, body: JSON.stringify({ message: "User not found" }) };
}
return { statusCode: 200, body: JSON.stringify(user) };
} catch (error) {
console.error("Database error:", error); // Log details
return {
statusCode: 500,
body: JSON.stringify({ message: "Something went wrong. Please try again later." })
};
}
}
4. ESLint Best Practices
Configuration Rules:
- No semicolons:
semi: ['error', 'never'] - 4-space indentation:
indent: ['error', 4] - Max line length: 320 characters
- Restricted:
no-console(exceptconsole.warn()andconsole.error())
Bad Practice:
/* eslint-disable no-await-in-loop */
/* eslint-disable no-console */
// Disabling rules for entire file
Good Practice:
// Selective disabling for specific cases
// eslint-disable-next-line no-console
console.log("Message successfully sent to SQS with ID:", messageId)
// Better approach: Use Promise.all() instead of disabling rules
await Promise.all(items.map(async (item) => processItem(item)))
API Management and Documentation
1. Understanding API Requirements
- Analyze purpose and reusability before creating new APIs
- Map API interactions with frontend components
- Create mock APIs for external providers without sandbox environments
- Define data flow and lifecycle clearly
2. API Components
Essential Elements:
- Endpoint: URL where API is hosted
- HTTP Methods: GET, POST, PUT, PATCH, DELETE
- Query Parameters: For filtering and sorting
- Path Parameters: For specific resource identification
- Request Body: Data sent in requests (JSON format)
- Headers: Metadata and authentication info
- Authorization: Bearer tokens for security
3. Proper API Design
Bad Practice:
POST /users/create β Creates a new user
GET /users/list β Retrieves users list
DELETE /users/remove/{id} β Deletes a user
Good Practice:
POST /users β Creates a new user
GET /users β Retrieves users list
DELETE /users/{id} β Deletes a user
4. HTTP Status Codes
Success Codes:
200 OK- Request successful201 Created- Resource created successfully202 Accepted- Request accepted for processing204 No Content- Successful, no content to return
Client Error Codes:
400 Bad Request- Malformed request401 Unauthorized- Authentication required403 Forbidden- Insufficient permissions404 Not Found- Resource doesnβt exist409 Conflict- Request conflicts with current state410 Gone- Resource expired/no longer available429 Too Many Requests- Rate limit exceeded
Server Error Codes:
500 Internal Server Error- Unexpected server condition502 Bad Gateway- Invalid upstream response503 Service Unavailable- Temporarily unavailable504 Gateway Timeout- Response timeout
Underutilized But Important Status Codes
202 Accepted - Request accepted for processing but not yet completed. Used for AWS Lambda async tasks or background batch jobs. Example: Large data export requests that are queued for processing.
410 Gone - Resource existed before but is permanently removed. Different from 404 (never existed). Example: Expired shopping carts that are no longer valid after timeout period.
424 Failed Dependency - Request failed because a dependent external service encountered an error. Example: Crypto exchange API fails, affecting conversion rate calculations.
429 Too Many Requests - User exceeded rate limits. Often returned when AWS API throttling limits are hit. Example: Client sends too many requests to API Gateway within rate limit window.
Database Management
1. Database Selection
Relational Databases (SQL):
- Amazon RDS (PostgreSQL, MySQL)
- Amazon Aurora (high-performance)
- Use for structured data with relationships
NoSQL Databases:
- Amazon DynamoDB (key-value, document)
- Amazon DocumentDB (MongoDB-compatible)
- Use for flexible, unstructured data
2. Connection Handling
- PostgreSQL/MySQL: Use connection pooling with Knex.js
- DynamoDB: HTTP-based, no connection management needed
- RDS Data API: Automatic connection handling
- Avoid frequent connection opening/closing
Security
1. API Authentication
Methods:
- Cognito Authentication with Auth ARN validation
- JWT-based authentication for custom implementations
- OAuth for third-party integrations
2. Authorization & Access Control
- Implement role-based access control
- Validate permissions at backend level, not just frontend
- Prevent privilege escalation attacks
- Consider AWS Verified Permissions service
// β
Always validate permissions server-side
if (user.role !== 'admin' && requestedAction === 'delete') {
return { statusCode: 403, message: "Insufficient permissions" };
}
Testing Strategies
1. Manual API Testing
Testing Approach:
- Initial validation - Send requests with no parameters
- Negative testing - Invalid/incomplete data
- Security testing - Unauthorized access attempts
- Success path validation - Valid requests
2. Automated Testing with Dredd
Key Principles:
- Validate APIs against OpenAPI/Swagger specifications
- Use dynamic test data instead of static responses
- Handle authentication and dynamic IDs properly
- Implement test data cleanup mechanisms
Avoid Static Responses:
// Bad: Static response bypasses actual logic
if (requestBody.email === 'test@example.com') {
return { statusCode: 201, body: staticResponse };
}
Use DEBUG Mode:
// Good: Conditional skipping while running most logic
const DEBUG = process.env.DEBUG === "true";
if (!(DEBUG && requestBody.email.includes("apitest"))) {
await sendSMS(requestBody.phone_number);
}
3. Promise Handling
Promise.all() vs Promise.allSettled():
- Promise.all(): Fails fast if any promise rejects
- Promise.allSettled(): Waits for all promises, collects results
// Use Promise.all() when all requests must succeed
const rates = await Promise.all(coins.map(coin => fetchRate(coin)));
// Use Promise.allSettled() when partial results are acceptable
const results = await Promise.allSettled(coins.map(coin => fetchRate(coin)));
const successfulRates = results.filter(res => res.status === 'fulfilled');
Key Takeaways
Essential Practices
- Use descriptive naming conventions consistently across projects
- Comment code to explain βwhy,β not βhowβ
- Design RESTful APIs with proper HTTP methods and status codes
- Implement robust error handling with user-friendly messages
- Secure APIs with proper authentication and authorization
- Test dynamically without static responses or hardcoded data
- Optimize database queries and use appropriate connection handling
Common Pitfalls to Avoid
Vague or inconsistent naming conventions
Disabling ESLint rules for entire files
Including action names in API endpoints
Returning static responses in API tests
Exposing internal error details to clients
Unnecessary database calls in loops
Frontend-only security validation
Additional Resources
- ID Tokens Vs Access Tokens
- ESLint Configuration Guide
- AWS Lambda Cold Start Optimization
- Cognito Custom Authentication Flow
- Automated API Testing with Dredd
Why This Matters: Following these best practices reduces errors, improves performance, enhances security, and maintains scalable architecture. Standardizing our approach ensures better collaboration, smoother workflows, and higher-quality software.
Building efficient, maintainable, and future-proof backend systems starts with solid foundations and consistent practices.