API Design Guidelines

Types of APIs Based on Architecture with Design Guidelines

1. RESTful APIs (Representational State Transfer)

Characteristics:

  • Stateless architecture (each request is independent).
  • Uses resource-oriented design (/users, /products).
  • Supports standard HTTP methods: GET, POST, PUT, DELETE.
  • Works over HTTP(S) and supports caching.
  • Preferred data format: JSON (sometimes XML).

Supporting Languages:

  • Java (Spring Boot, Jersey)
  • Python (Flask, Django REST Framework)
  • JavaScript (Node.js with Express)
  • Go (Gin, Echo)
  • Ruby (Ruby on Rails)

Use Cases:

✔ Fetching user details in a mobile app (e.g., Twitter API).
✔ Managing cloud resources in AWS, Azure.

Design Guidelines:

Use Nouns for Resource Naming. 

Avoid like /getUsers, /createOrder. Keep URLs Short & Readable

  • /users → Get all users
  • /users/{id} → Get a specific user
  • /orders/{orderId}/items → Get items in an order

Use Proper HTTP Methods

  • GET /users → Fetch users.
  • POST /users → Create a user.
  • PUT /users/123 → Update user with ID 123.
  • PATCH /users/123 → Partially (only specific fields) update user with ID 123.
  • DELETE /users/123 → Delete user with ID 123.
Use Query Parameters for Filtering, Sorting & Pagination
  • Good: /products?category=electronics&sort=price_desc&page=1&limit=10
  • Bad:   /getProductsByCategory/electronics/sortByPriceDescending
Version Your API
  • Use URL versioning (/api/v1/users)
  • Alternatively, use header-based versioning (Accept: application/vnd.api+json; version=1.0)
Use Meaningful Status Codes
  • 200 OK  Success (GET, PUT, PATCH, DELETE).
  • 201 Created  Resource successfully created (POST)
  • 204 No Content → Success, no response body (DELETE)
  • 400 Bad Request → Invalid request parameters
  • 401 Unauthorized → Authentication failed
  • 403 Forbidden → No permission to access
  • 404 Not Found → Resource doesn't exist
  • 409 Conflict → Data conflict (e.g., duplicate entry)
  • 500 Internal Server Error → Server issue
Use Consistent and Predictable Responses
  • Good JSON Response (Structured and Consistent)
{
  "status": "success",
  "data": {
    "id": 123,
    "name": "John Doe",
    "email": "john@example.com"
  }
}

  • Bad JSON Response (Unstructured)
 {
  "123": "John Doe",
  "email": "john@example.com"
}

✅ Use Handle Errors Gracefully
  • Good Error Response
{
  "status": "error",
  "message": "Invalid email format",
  "code": 400
}
  • Bad Error Response
{
           "error": "Oops! Something went wrong."
          }

✅ Secure Your API
  • Use Authentication & Authorization:
    • OAuth 2.0, JWT (JSON Web Tokens), API Keys
    • Use RBAC (Role-Based Access Control)
  • Use HTTPS:
    • Always enforce HTTPS to encrypt communication.
  • Rate Limiting & Throttling:
    • Prevent abuse using rate limits (e.g., 100 requests per minute per user).
  • Validate and Sanitize Input:
    • Prevent SQL Injection, XSS, CSRF attacks.
  • Use Secure Headers (CORS, CSP, etc.)
    • Set Content Security Policy (CSP) and CORS headers properly.
Implement Caching (ETag, Last-Modified headers, Redis, CDN).
  • Use ETag headers to avoid unnecessary re-fetching
  • Implement Redis or CDN caching for high-traffic APIs
✅ Logging & Monitoring
  • Use structured logs (JSON format) for debugging.
  • Implement API monitoring tools (e.g., AWS CloudWatch, Datadog, Prometheus).
✅ HATEOAS for Discoverability → With HATEOAS, the API responses provide navigational links, allowing clients to discover related resources dynamically.
  • GET /employees/1
          {
              "id": 1,
    "name": "Alice",
    "role": "Developer",
    "_links": {
        "self": {
            "href": "http://localhost:8080/employees/1"
        },
        "all-employees": {
            "href": "http://localhost:8080/employees"
        }
    }
}
Optimize performance with multiple strategies
  • Gzip Compression 
    • Gzip reduces the size of HTTP responses, minimizing bandwidth usage and improving response times.
    • Enable Gzip compression in a Spring Boot application
      • server.compression.enabled=true 
      • server.compression.mime-types=text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json
      • server.compression.min-response-size=1024
  • Connection Pooling
    • Instead of creating a new connection for every request, a connection pool manages and reuses existing connections, reducing overhead.
    • In Java, you can use HikariCP (for databases) or Apache HttpClient with connection pooling for HTTP calls.
    • Example of connection pooling with Apache HttpClient (You can also use Spring WebClient for creating connection pooling)
      • PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
      • cm.setMaxTotal(100);  // Set max connections
      • cm.setDefaultMaxPerRoute(10);  // Set max per route
      • CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();
    • Example HikariCP with an MSSQL (Microsoft SQL Server) database in a Spring Boot application is straightforward. HikariCP is the default connection pool in Spring Boot, but you can configure it explicitly for fine-tuned performance.
      • Add maven dependencies
<dependency>
    <groupId>com.microsoft.sqlserver</groupId>
    <artifactId>mssql-jdbc</artifactId>
    <version>12.2.0.jre11</version>  
</dependency>

<dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>5.1.0</version>  
</dependency>

      • Spring Boot automatically detects HikariCP when present in the classpath, so you can configure it in application.properties (You can configure HikariCP Programmatically as well)

# MSSQL Database Configuration
spring.datasource.url=jdbc:sqlserver://your-server-name:1433;databaseName=your_database;encrypt=true;trustServerCertificate=true
spring.datasource.username=your_username
spring.datasource.password=your_password
spring.datasource.driver-class-name=com.microsoft.sqlserver.jdbc.SQLServerDriver

# HikariCP Configuration
spring.datasource.hikari.maximum-pool-size=10  # Max connections in pool
spring.datasource.hikari.minimum-idle=5        # Minimum idle connections
spring.datasource.hikari.idle-timeout=30000    # 30 seconds idle timeout
spring.datasource.hikari.connection-timeout=30000  # 30 seconds to wait for connection
spring.datasource.hikari.max-lifetime=1800000  # 30 minutes max connection lifetime
      • Verify Connection Pooling
                              To check if HikariCP is working, look for the following logs at application startup
                               com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Starting...
                               com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed.
      • Use it with JdbcTemplate
                                @Service 
                                public class UserService
                                    private final JdbcTemplate jdbcTemplate; 
                                    public UserService(JdbcTemplate jdbcTemplate)
                                       this.jdbcTemplate = jdbcTemplate; 
                                    
                                    public List<Map<String, Object>> getAllUsers()
                                       return jdbcTemplate.queryForList("SELECT * FROM users"); 
                                    } 
                                }
      • Performance Tuning Tips
        • Increase maximum-pool-size if you expect high traffic.
        • Reduce connection-timeout to avoid slow connections.
        • Enable MSSQL connection pooling with encrypt=true;trustServerCertificate=true for security.
        • Set validationQuery to check if connections are still valid before using them.

✅ Handle Concurrency Issues → Use ETags, optimistic locking, or pessimistic locking.

Handling concurrency issues effectively ensures data consistency and prevents conflicts in multi-user or distributed systems. Here’s how you can use ETags, optimistic locking, or pessimistic locking:

1. ETags (Entity Tags) – HTTP Caching & Conditional Requests

    • Used in REST APIs to prevent lost updates.
    • The server generates an ETag (usually a hash of the resource).
    • Clients include the ETag in requests (If-Match header).
    • If the resource is modified by another client, the update is rejected.

                Best for: Web APIs, caching, and detecting resource changes.

2. Optimistic Locking – Version-based Conflict Detection

    • Each record has a version number or timestamp.
    • When updating, the system checks if the version matches.
    • If another process modified the record, the update is rejected or retried.

                Best for: High-read, low-write scenarios (e.g., financial transactions).

              Example: Optimistic Locking in a Spring Boot application using JPA and @Version annotation
          
              Step1: Entity Class with Optimistic Locking
    @Entity
    @Data
    public class Product {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        private String name;
        private double price;

        @Version  // Enables Optimistic Locking
        private int version;
    }
    • @Version ensures that when a record is updated, the version is checked.
    • If another transaction modified the record, a OptimisticLockingFailureException is thrown.
            Step2: Repository Interface
            public interface ProductRepository extends JpaRepository<Product, Long> { }

            Step3: Service Layer with Update Handling
            @Service
   public class ProductService {
   private final ProductRepository productRepository;
   public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
   } 
    
   @Transactional
    public void updateProductPrice(Long productId, double newPrice) {
        Product product = productRepository.findById(productId)
                .orElseThrow(() -> new RuntimeException("Product not found"));
        product.setPrice(newPrice);
        productRepository.save(product);  // Spring handles version checking
    }
  
        
             Step4: Handling Concurrency Exception in Controller
             @RestController
   @RequestMapping("/products")
    public class ProductController { 
        private final ProductService productService;
        public ProductController(ProductService productService) {
            this.productService = productService;
        }

    @PutMapping("/{id}/price")
    public String updatePrice(@PathVariable Long id, @RequestParam double price) {
        try {
            productService.updateProductPrice(id, price);
            return "Product price updated successfully!";
        } catch (OptimisticLockingFailureException e) {
            return "Update failed due to concurrent modification. Please retry.";
        }
    }
 
    How It Works
    1. Multiple users fetch the same product (e.g., GET /products/1).
    2. Both try updating price concurrently.
    3. The first update succeeds, incrementing the version field.
    4. The second update fails because the version has changed.
    5. The API responds: "Update failed due to concurrent modification. Please retry."
        This approach avoids lost updates while ensuring high performance!

         3. Pessimistic Locking – Exclusive Access Control

    • The system locks the record when a process starts updating.
    • Other processes must wait until the lock is released.
    • Implemented using database locks (SELECT ... FOR UPDATE).
            Best for: High-write contention scenarios (e.g., booking systems).

            Example: Pessimistic Locking example using Spring Boot, JPA, nd @Lock(LockModeType.PESSIMISTIC_WRITE).

Step1 Entity Class
@Entity
@Data
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private double price;
}

        Step2: Repository with Pessimistic Locking
@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
@Lock(LockModeType.PESSIMISTIC_WRITE) // Locks row until transaction completes
@Query("SELECT p FROM Product p WHERE p.id = :id")
Optional<Product> findByIdWithLock(Long id);
}
  • @Lock(LockModeType.PESSIMISTIC_WRITE): Ensures that no other transaction can modify or read the record until the current transaction is complete.
        Step3: Service Layer Handling Updates
    @Service     public class ProductService { private final ProductRepository productRepository; public ProductService(ProductRepository productRepository) { this.productRepository = productRepository; } @Transactional public void updateProductPrice(Long productId, double newPrice) { Product product = productRepository.findByIdWithLock(productId) .orElseThrow(() -> new RuntimeException("Product not found")); // Simulating long-running operation (optional) try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } product.setPrice(newPrice); productRepository.save(product);   }     }
  • Locks the record when fetching it, preventing other transactions from reading or updating it until this one completes.
  • The Thread.sleep(5000); simulates a delay, making it easier to see how other transactions are blocked.
        Step4: Controller with Update Endpoint
@RestController
@RequestMapping("/products")
public class ProductController {
private final ProductService productService;
public ProductController(ProductService productService) {
this.productService = productService;
}
@PutMapping("/{id}/price")
public String updatePrice(@PathVariable Long id, @RequestParam double price) {
productService.updateProductPrice(id, price);
return "Product price updated successfully!";
}
}
How It Works
    1. User A calls PUT /products/1/price?price=150.0, locking the record for 5 seconds.
    2. User B tries to update the same product within those 5 seconds but gets blocked until User A’s transaction completes.
    3. Once User A’s transaction commits, User B’s request is processed.
            
When to Use Pessimistic Locking?

    • High contention scenarios (e.g., inventory management, banking transactions).
    • Critical data updates where lost updates cannot be tolerated.
    • Not ideal for high-read applications due to performance overhead.

✅ Return 204 for Successful DELETE → No need to return a body.

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT) // Directly sets 204 status
    public void deleteItem(@PathVariable Long id) {
        // Logic to delete the item from the database
    }

✅ Idempotency for Safe Requests → Ensure PUT and DELETE are repeatable without side effects.

Idempotency ensures that repeated PUT and DELETE requests do not cause unintended side effects.

  • PUT should update or create a resource if it doesn’t exist.
  • DELETE should be repeatable, meaning multiple DELETE calls should not fail.
Approach
  1. PUT Idempotency:

    • Use the resource ID for updates.
    • If the resource exists, update it.
    • If it doesn’t exist, create it (Upsert).
  2. DELETE Idempotency:

    • Deleting a resource should not fail if it is already deleted.

✅ Use Background Processing for Long Tasks → Don’t block API for time-consuming operations.

Long-running tasks should not block the API response. Instead, you can handle them in the background using asynchronous processing.

Use @Async for Background Processing: Spring provides the @Async annotation to execute methods asynchronously.

Step 1: Enable Async Processing 
@Configuration 
@EnableAsync 
public class AsyncConfig { }

Step 2: Create an Async Service
@Service 
public class BackgroundTaskService
  @Async // This method runs asynchronously 
  public void longRunningTask()
      try { Thread.sleep(5000); // Simulating a long task 
          System.out.println("Task Completed!"); 
     } catch (InterruptedException e) { 
         Thread.currentThread().interrupt(); } 
     } 

Step 3: Call Async Method 
@RequestMapping("/api") 
public class AsyncController
  private final BackgroundTaskService taskService; 
  public AsyncController(BackgroundTaskService taskService)
      this.taskService = taskService; 
 } 

@GetMapping("/start-task") 
  public String triggerTask()
  taskService.longRunningTask(); // Runs in background return "Task started!"
 }

✅ Provide API Documentation → Use Swagger/OpenAPI, Postman Collections for clarity and usability.  

 

2. SOAP APIs (Simple Object Access Protocol)

Characteristics:

  • XML-based strict messaging format.
  • Works over HTTP, SMTP, TCP.
  • Supports WS-Security (encryption, authentication).
  • Uses WSDL (Web Services Description Language) for defining operations.
  • High reliability and transaction support.

Supporting Languages:

  • Java (JAX-WS)
  • C# (.NET WCF)
  • Python (Zeep)
  • PHP (SOAP extension)

Use Cases:

✔ Secure banking transactions (fund transfers, credit score checks).
✔ Healthcare system integrations (hospital & pharmacy data exchange).

Design Guidelines:

Use Strong XML Schema (XSD) for defining strict data formats.
Follow WSDL Structure (Defines available operations and endpoints).
Use WS-Security for Encryption & Authentication.
Ensure Message Integrity (Use XML Signature for verification).
Use MTOM (Message Transmission Optimization Mechanism) for handling large files.
Implement SOAP Fault Messages for Errors (Instead of HTTP status codes).


3. GraphQL APIs

Characteristics:

  • Clients request only the required data (reduces over-fetching).
  • Uses a single endpoint (/graphql).
  • Supports real-time updates via subscriptions.
  • Strongly typed schema with flexible queries.
  • High performance for data-intensive applications.

Supporting Languages:

  • JavaScript (Apollo Server, GraphQL.js)
  • Python (Graphene)
  • Java (GraphQL Java)
  • Go (gqlgen)
  • Ruby (GraphQL Ruby)

Use Cases:

✔ Fetching specific product details in an e-commerce app (e.g., Shopify API).
✔ Optimized social media feed retrieval (e.g., Facebook API).

Design Guidelines:

Use a Strongly Typed Schema (Define queries, mutations, subscriptions).
Support Query Batching & Caching (Optimize performance).
Limit Query Depth to Prevent Overloading (Avoid deep nested queries).
Use Pagination for Large Data Fetches.
Implement Authorization per Field or Object Level.
Use Proper Naming Conventions (CamelCase for fields, PascalCase for types).


4. gRPC (Google Remote Procedure Call)

Characteristics:

  • Uses Protocol Buffers (protobuf) for fast, compact data serialization.
  • Supports bidirectional streaming over HTTP/2.
  • Low latency and high performance.
  • Ideal for microservices communication.
  • Encrypted communication using TLS.

Supporting Languages:

  • Java (gRPC-Java)
  • Go (gRPC-Go)
  • Python (gRPC-Python)
  • C++ (gRPC-C++)
  • JavaScript (gRPC-Web)

Use Cases:

✔ Fast inter-service communication in Netflix, Uber.
✔ IoT devices communicating with cloud platforms.

Design Guidelines:

Define Clear Service Contracts in Protobuf (.proto files).
Use Unary, Client Streaming, Server Streaming, and Bidirectional Streaming as Needed.
Enable TLS Encryption for Security.
Optimize gRPC Message Size for Performance.
Use Load Balancing in gRPC for Scaling Services.
Handle gRPC Errors Properly (Status Codes & Exception Handling).


5. WebSockets APIs

Characteristics:

  • Real-time, bidirectional communication over a persistent connection.
  • Works over single TCP connection (low latency).
  • Used for live data streaming (chat apps, stock market, multiplayer gaming).
  • Supports text (JSON) and binary messages.

Supporting Languages:

  • JavaScript (Socket.io, WebSockets API)
  • Python (WebSockets, FastAPI)
  • Java (Spring WebSocket)
  • Golang (Gorilla WebSocket)
  • C# (.NET SignalR)

Use Cases:

✔ Real-time chat applications (e.g., WhatsApp, Slack).
✔ Live stock market updates (e.g., Binance, TradingView).

Design Guidelines:

Establish Secure WebSocket Connections (Use WSS for Encryption).
Handle Connection Lifecycle Properly (Open, Close, Reconnect).
Use Message Broadcasting for Group Communications.
Implement Heartbeats/Ping Mechanism for Connection Health.
Rate Limit Messages to Prevent Flooding.
Graceful Disconnection Handling.


Comparison Table of API Architectures



Conclusion

Each API architecture has specific use cases:

  • Use REST for general-purpose web/mobile applications.
  • Use SOAP for highly secure enterprise applications.
  • Use GraphQL for optimized data fetching.
  • Use gRPC for high-performance, low-latency microservices.
  • Use WebSockets for real-time applications like chat and live stock updates.