Backend Development: Best Practices and Guidelines

A comprehensive guide to backend development best practices, common pitfalls, and essential guidelines for building robust, scalable, and maintainable applications.

:open_book: 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.

:wrench: Coding Best Practices

1. Naming Conventions

:white_check_mark: Good Practices:

  • Use descriptive names that convey intent
  • Follow camelCase for variables/functions, PascalCase for classes
  • Avoid abbreviations unless widely accepted (userId instead of uid)
  • Ensure names are self-explanatory (isUserActive instead of flag1)
// βœ… 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 (except console.warn() and console.error())

:cross_mark: Bad Practice:

/* eslint-disable no-await-in-loop */
/* eslint-disable no-console */
// Disabling rules for entire file

:white_check_mark: 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)))

:globe_with_meridians: 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

:cross_mark: Bad Practice:

POST /users/create     β†’ Creates a new user
GET /users/list        β†’ Retrieves users list  
DELETE /users/remove/{id} β†’ Deletes a user

:white_check_mark: 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 successful
  • 201 Created - Resource created successfully
  • 202 Accepted - Request accepted for processing
  • 204 No Content - Successful, no content to return

Client Error Codes:

  • 400 Bad Request - Malformed request
  • 401 Unauthorized - Authentication required
  • 403 Forbidden - Insufficient permissions
  • 404 Not Found - Resource doesn’t exist
  • 409 Conflict - Request conflicts with current state
  • 410 Gone - Resource expired/no longer available
  • 429 Too Many Requests - Rate limit exceeded

Server Error Codes:

  • 500 Internal Server Error - Unexpected server condition
  • 502 Bad Gateway - Invalid upstream response
  • 503 Service Unavailable - Temporarily unavailable
  • 504 Gateway Timeout - Response timeout

:light_bulb: Underutilized But Important Status Codes


:light_bulb: 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.

:light_bulb: 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.

:light_bulb: 424 Failed Dependency - Request failed because a dependent external service encountered an error. Example: Crypto exchange API fails, affecting conversion rate calculations.

:light_bulb: 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.

:file_cabinet: 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

:locked: 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" };
}

:test_tube: Testing Strategies

1. Manual API Testing

Testing Approach:

  1. Initial validation - Send requests with no parameters
  2. Negative testing - Invalid/incomplete data
  3. Security testing - Unauthorized access attempts
  4. 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

:cross_mark: Avoid Static Responses:

// Bad: Static response bypasses actual logic
if (requestBody.email === 'test@example.com') {
  return { statusCode: 201, body: staticResponse };
}

:white_check_mark: 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');

:bullseye: Key Takeaways

:white_check_mark: 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

:police_car_light: Common Pitfalls to Avoid

  • :cross_mark: Vague or inconsistent naming conventions
  • :cross_mark: Disabling ESLint rules for entire files
  • :cross_mark: Including action names in API endpoints
  • :cross_mark: Returning static responses in API tests
  • :cross_mark: Exposing internal error details to clients
  • :cross_mark: Unnecessary database calls in loops
  • :cross_mark: Frontend-only security validation

:books: Additional Resources


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.

3 Likes