Thursday, May 22, 2025

Python API interview questions and answers

 

Here are the some questions about API that are usually asked in Python coding interview Keep in mind that for many questions, there can be multiple valid approaches, and the provided answers represent common or best practices.


I. Core Python Concepts (as they relate to APIs)

These questions test your foundational Python knowledge, which is crucial for building robust APIs.

  1. Data Structures:

    • Question: Explain the difference between list, tuple, dict, and set in Python and when you would choose one over the other in the context of API request/response handling.
    • Answer:
      • list: Ordered, mutable, allows duplicate elements. Good for ordered collections of items, e.g., a list of product IDs in a shopping cart request, or a sequence of events in an API response.
      • tuple: Ordered, immutable, allows duplicate elements. Good for fixed collections of related items where the order and content should not change, e.g., representing coordinates (latitude, longitude), or fixed configuration parameters that an API endpoint might expect. Often used as dictionary keys when order matters and immutability is desired.
      • dict: Unordered (since Python 3.7, insertion order is preserved), mutable, stores key-value pairs, keys must be unique and immutable. Essential for JSON payloads in API requests and responses, as JSON naturally maps to dictionaries. E.g., {"name": "Alice", "age": 30}.
      • set: Unordered, mutable, stores unique elements. Good for checking membership efficiently or removing duplicates from a collection, e.g., to ensure a list of user permissions passed to an API has no duplicates, or to find common elements between two sets of data.
  2. Functions and Decorators:

    • Question: What are Python decorators, and how can they be used in an API context (e.g., for authentication, logging, or rate limiting)? Provide an example.

    • Answer:

      • What are Decorators: Decorators are a way to modify or enhance the behavior of functions or methods without explicitly changing their source code. They are essentially functions that take another function as an argument, add some functionality, and return a new function (or the modified original).

      • API Context Usage:

        • Authentication/Authorization: A decorator can check if a user is logged in or has the necessary permissions before allowing access to an API endpoint.
        • Logging: Automatically log every API request, including URL, method, and parameters.
        • Rate Limiting: Track and enforce limits on how many requests a client can make within a certain timeframe.
        • Input Validation: Validate incoming request data before the main logic of the API endpoint is executed.
      • Example (Authentication):

        Python
        from functools import wraps
        from flask import request, jsonify
        
        # Dummy user check (in a real app, you'd check a database/token)
        AUTHORIZED_USERS = {"admin_token", "user_token"}
        
        def authenticate_token(f):
            @wraps(f)
            def decorated_function(*args, **kwargs):
                token = request.headers.get('Authorization')
                if not token or token.replace("Bearer ", "") not in AUTHORIZED_USERS:
                    return jsonify({"message": "Unauthorized"}), 401
                return f(*args, **kwargs)
            return decorated_function
        
        # Example Flask API usage
        # @app.route('/protected_data')
        # @authenticate_token
        # def protected_data():
        #     return jsonify({"data": "This is sensitive data."})
        
    • Question: Explain *args and **kwargs and how they are useful when designing flexible API endpoints that can accept varying parameters.

    • Answer:

      • *args (Arbitrary Positional Arguments): Allows a function to accept an arbitrary number of positional arguments. These arguments are collected into a tuple.

      • **kwargs (Arbitrary Keyword Arguments): Allows a function to accept an arbitrary number of keyword arguments. These arguments are collected into a dictionary.

      • Usefulness in APIs: They are crucial for creating flexible and extensible API endpoints.

        • Forwarding Arguments: An API endpoint might receive parameters and need to forward them to an internal service or another function without knowing all possible parameters upfront.
        • Generic Handlers: You can write generic handlers that can process different types of requests, where the specific parameters vary.
        • Middleware/Decorators: Decorators or middleware functions can accept *args and **kwargs to wrap any function, regardless of its signature, and pass through all its original arguments.
      • Example:

        Python
        def process_request_params(endpoint_name, *args, **kwargs):
            print(f"Processing request for: {endpoint_name}")
            print(f"Positional arguments: {args}")
            print(f"Keyword arguments: {kwargs}")
        
            if 'user_id' in kwargs:
                print(f"User ID found: {kwargs['user_id']}")
            if args:
                print(f"First positional arg: {args[0]}")
        
        # API endpoint could call this with varying parameters
        process_request_params("get_user", 123, "active", name="Alice", status="online")
        # Output:
        # Processing request for: get_user
        # Positional arguments: (123, 'active')
        # Keyword arguments: {'name': 'Alice', 'status': 'online'}
        # User ID found: 123
        # First positional arg: 123
        
  3. Classes and Object-Oriented Programming (OOP):

    • Question: How would you design a Python class to represent a "Resource" in a RESTful API (e.g., a Product or User object)? What methods and attributes would it have?

    • Answer:

      • Design Rationale: In a RESTful API, resources are the core entities. Representing them as Python classes allows for clean, organized code, encapsulation of data and behavior, and easier mapping to database models.

      • Example Product Class:

        Python
        class Product:
            def __init__(self, product_id, name, description, price, stock_quantity=0):
                self.product_id = product_id
                self.name = name
                self.description = description
                self.price = price
                self.stock_quantity = stock_quantity
        
            def to_dict(self):
                """Converts the Product object to a dictionary for JSON serialization."""
                return {
                    "id": self.product_id,
                    "name": self.name,
                    "description": self.description,
                    "price": self.price,
                    "stock_quantity": self.stock_quantity
                }
        
            @classmethod
            def from_dict(cls, data):
                """Creates a Product object from a dictionary (e.g., from an API request body)."""
                if not all(k in data for k in ['id', 'name', 'price']):
                    raise ValueError("Missing essential product data.")
                return cls(
                    product_id=data['id'],
                    name=data['name'],
                    description=data.get('description', ''),
                    price=data['price'],
                    stock_quantity=data.get('stock_quantity', 0)
                )
        
            def update(self, new_data):
                """Updates product attributes based on new data."""
                for key, value in new_data.items():
                    if hasattr(self, key): # Only update existing attributes
                        setattr(self, key, value)
        
            def is_in_stock(self):
                """Checks if the product is in stock."""
                return self.stock_quantity > 0
        
            def decrease_stock(self, quantity):
                """Decreases the stock quantity."""
                if self.stock_quantity >= quantity:
                    self.stock_quantity -= quantity
                    return True
                return False
        
      • Attributes:

        • product_id: Unique identifier (e.g., UUID or integer).
        • name: String.
        • description: String.
        • price: Float or Decimal.
        • stock_quantity: Integer.
        • created_at, updated_at: Datetime objects (for auditing).
      • Methods:

        • __init__: Constructor to initialize product attributes.
        • to_dict(): Converts the object into a dictionary, suitable for JSON serialization in API responses.
        • from_dict() (classmethod): Creates an object from a dictionary, useful for parsing incoming JSON request bodies.
        • update(): Modifies attributes based on partial data (for PATCH requests).
        • is_in_stock(), decrease_stock(): Business logic related to the product.
    • Question: Discuss the concept of inheritance and polymorphism in Python and how they might be applied to API design (e.g., different types of users having different permissions).

    • Answer:

      • Inheritance: Allows a new class (subclass/child) to inherit attributes and methods from an existing class (superclass/parent). This promotes code reuse and a hierarchical structure.

        • API Application:
          • User Roles: You could have a base User class and then AdminUser, CustomerUser, GuestUser subclasses. Each subclass could inherit basic user properties but override or add methods for specific permissions or behaviors.
          • Resource Types: A base APIResource class could define common methods (get_by_id, create, update, delete) and then specific resources like ProductResource or OrderResource could inherit from it, providing specific implementations for their data.
      • Polymorphism: "Many forms." It refers to the ability of different classes to respond to the same method call in different ways. In Python, this is often achieved through method overriding or duck typing.

        • API Application:
          • Request Handlers: Imagine an API endpoint that processes different types of "events" (e.g., OrderCreatedEvent, UserLoggedInEvent). Each event type could be an object with a process() method. The API handler could iterate through a list of events and call event.process() on each, and the specific implementation for processing would vary based on the event's class.
          • Serializers/Transformers: If you have different types of objects that need to be serialized into a standard JSON format, you could define a common serialize() method. Even if the internal data structure differs, calling obj.serialize() on different object types would produce the desired output.
      • Example (User Roles):

        Python
        class BaseUser:
            def __init__(self, user_id, username):
                self.user_id = user_id
                self.username = username
        
            def can_view(self, resource_type):
                return False # Default: no view access
        
            def to_dict(self):
                return {"id": self.user_id, "username": self.username}
        
        class CustomerUser(BaseUser):
            def can_view(self, resource_type):
                return resource_type in ["products", "orders", "profile"]
        
            def place_order(self, order_details):
                # Logic to place an order
                print(f"{self.username} placing order: {order_details}")
                return True
        
        class AdminUser(BaseUser):
            def can_view(self, resource_type):
                return True # Admins can view everything
        
            def manage_users(self, action):
                # Logic for admin actions
                print(f"{self.username} performing admin action: {action}")
                return True
        
        # In an API handler:
        # user_obj = CustomerUser(1, "Alice") or AdminUser(2, "Bob")
        # if user_obj.can_view("products"):
        #    # Allow access to products endpoint
        # if isinstance(user_obj, AdminUser):
        #    user_obj.manage_users("delete_old_accounts")
        
  4. Error Handling and Exceptions:

    • Question: How do you handle exceptions in Python, and how would you use try-except-finally blocks to gracefully handle errors in an API endpoint (e.g., invalid input, database connection issues)?

    • Answer:

      • Exception Handling: Python uses try, except, else, and finally blocks to manage errors.

        • try: The code that might raise an exception is placed here.
        • except: If an exception occurs in the try block, the code in the matching except block is executed. You can catch specific exception types.
        • else: (Optional) The code in the else block is executed if no exception occurs in the try block.
        • finally: (Optional) The code in the finally block is always executed, regardless of whether an exception occurred or not. It's often used for cleanup1 operations (e.g., closing file handles, database connections).
      • API Context:

        • Invalid Input: Catch ValueError, TypeError, or custom validation exceptions.
        • Database Issues: Catch OperationalError (for connection issues), IntegrityError (for unique constraint violations), etc.
        • External Service Calls: Catch requests.exceptions.RequestException for network or HTTP errors.
      • Example:

        Python
        from flask import Flask, request, jsonify
        import sqlite3 # Example database library
        
        app = Flask(__name__)
        DATABASE = 'my_api.db'
        
        def get_db_connection():
            conn = sqlite3.connect(DATABASE)
            conn.row_factory = sqlite3.Row # Allows accessing columns by name
            return conn
        
        @app.route('/users/<int:user_id>', methods=['GET'])
        def get_user(user_id):
            conn = None # Initialize conn to None
            try:
                conn = get_db_connection()
                cursor = conn.cursor()
                cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,))
                user = cursor.fetchone()
        
                if user is None:
                    return jsonify({"message": f"User with ID {user_id} not found"}), 404
                else:
                    return jsonify(dict(user)), 200 # Convert Row object to dict
        
            except sqlite3.OperationalError as e:
                # Database connection or query error
                return jsonify({"message": f"Database error: {str(e)}"}), 500
            except ValueError as e:
                # For cases where input parsing might fail before DB
                return jsonify({"message": f"Invalid input: {str(e)}"}), 400
            except Exception as e:
                # Catch any other unexpected errors
                print(f"An unexpected error occurred: {e}")
                return jsonify({"message": "An internal server error occurred"}), 500
            finally:
                if conn:
                    conn.close() # Ensure database connection is closed
        
        # To run:
        # app.run(debug=True)
        # Create a dummy table for testing:
        # conn = sqlite3.connect(DATABASE)
        # cursor = conn.cursor()
        # cursor.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
        # cursor.execute("INSERT INTO users (name) VALUES ('Alice')")
        # cursor.execute("INSERT INTO users (name) VALUES ('Bob')")
        # conn.commit()
        # conn.close()
        
    • Question: What are common HTTP status codes for errors (e.g., 400, 401, 403, 404, 500) and how would you map Python exceptions to these status codes in an API response?

    • Answer:

      • Common Error Status Codes:

        • 400 Bad Request: The client sent an invalid request (e.g., malformed JSON, missing required parameters, invalid data types).
        • 401 Unauthorized: The client is not authenticated (i.e., they haven't provided valid credentials like an API key or token).
        • 403 Forbidden: The client is authenticated but does not have the necessary permissions to access the resource or perform the action.
        • 404 Not Found: The requested resource could not be found on the server.
        • 405 Method Not Allowed: The HTTP method used (e.g., POST) is not supported for the requested resource.
        • 409 Conflict: The request could not be completed due to a conflict with the current state of the resource2 (e.g., trying to create a resource that already exists3 with a unique ID).
        • 429 Too Many Requests: The client has sent too many requests in a given amount of time (rate limiting).
        • 500 Internal Server Error: A generic error message4 indicating an unexpected condition on the server that prevented the fulfillment of the request. This is a catch-all for unhandled server-side exceptions.
        • 503 Service Unavailable: The server is currently unable to handle the request due to temporary overloading or maintenance.5
      • Mapping Python Exceptions:

        Python
        from flask import jsonify
        
        # Custom Exception Classes for API Errors
        class APIError(Exception):
            status_code = 500
            def __init__(self, message, status_code=None, payload=None):
                super().__init__(self)
                self.message = message
                if status_code is not None:
                    self.status_code = status_code
                self.payload = payload
        
            def to_dict(self):
                rv = dict(self.payload or ())
                rv['message'] = self.message
                return rv
        
        class BadRequest(APIError):
            def __init__(self, message="Bad Request", payload=None):
                super().__init__(message, status_code=400, payload=payload)
        
        class Unauthorized(APIError):
            def __init__(self, message="Unauthorized", payload=None):
                super().__init__(message, status_code=401, payload=payload)
        
        class Forbidden(APIError):
            def __init__(self, message="Forbidden", payload=None):
                super().__init__(message, status_code=403, payload=payload)
        
        class NotFound(APIError):
            def __init__(self, message="Resource Not Found", payload=None):
                super().__init__(message, status_code=404, payload=payload)
        
        # In a Flask application (example of a global error handler)
        # @app.errorhandler(APIError)
        # def handle_api_error(error):
        #     response = jsonify(error.to_dict())
        #     response.status_code = error.status_code
        #     return response
        
        # Example usage in an endpoint:
        # @app.route('/resource/<int:id>')
        # def get_resource(id):
        #    if not is_valid_id(id):
        #        raise BadRequest("Invalid resource ID format.")
        #    resource = db.get_resource(id)
        #    if not resource:
        #        raise NotFound(f"Resource with ID {id} does not exist.")
        #    if not current_user.can_access(resource):
        #        raise Forbidden("You do not have permission to access this resource.")
        #    return jsonify(resource.to_dict())
        
  5. Generators and Iterators:

    • Question: What are Python generators, and when might you use them in an API (e.g., for streaming large datasets, or processing data efficiently without loading everything into memory)?
    • Answer:
      • Generators: Functions that contain one or more yield statements. When called, they return an iterator, but they don't execute the entire function at once. Instead, they pause execution at each yield statement, return a value, and resume from where they left off the next time next() is called on the iterator. They are memory-efficient because they produce values on demand ("lazy evaluation") rather than building a full list in memory.

      • API Usage:

        • Streaming Large Datasets: When an API needs to return a very large amount of data (e.g., millions of log entries, financial transactions), loading it all into memory first can cause out-of-memory errors and high latency. A generator can yield data chunks, allowing the API to stream the response to the client incrementally.
        • Efficient Data Processing: If you're processing data within an API before sending it back (e.g., filtering, transforming), and the intermediate data is large, generators can prevent memory bottlenecks.
        • Reading Large Files: If an API reads large files (CSV, JSONL) from disk to serve, generators can read line by line or chunk by chunk.
      • Example (Streaming a large CSV file):

        Python
        from flask import Flask, Response, stream_with_context
        import csv
        import io
        
        app = Flask(__name__)
        
        # Simulate a large dataset
        def generate_large_data(num_records=100000):
            for i in range(num_records):
                yield {"id": i, "name": f"User {i}", "email": f"user{i}@example.com"}
        
        @app.route('/stream_users_csv')
        def stream_users_csv():
            def generate():
                data = io.StringIO()
                writer = csv.writer(data)
                writer.writerow(['id', 'name', 'email']) # Header row
                yield data.getvalue()
                data.seek(0)
                data.truncate(0)
        
                for user in generate_large_data(num_records=100000):
                    writer.writerow([user['id'], user['name'], user['email']])
                    yield data.getvalue()
                    data.seek(0)
                    data.truncate(0)
        
            response = Response(stream_with_context(generate()), mimetype='text/csv')
            response.headers["Content-Disposition"] = "attachment; filename=users.csv"
            return response
        
  6. Concurrency and Asynchronous Programming:

    • Question: Explain the Global Interpreter Lock (GIL) in Python and its implications for multi-threaded API applications. How do frameworks like asyncio help overcome this limitation?

    • Answer:

      • Global Interpreter Lock (GIL): The GIL is a mutex (mutual exclusion lock) that protects access to Python objects, preventing multiple native threads from executing Python bytecodes6 at once. This means that even7 on a multi-core processor, only one thread can execute Python bytecode at any given time.
      • Implications for Multi-threaded API Applications:
        • CPU-bound tasks: For tasks that are heavily CPU-bound (e.g., complex calculations, heavy data processing), multi-threading in Python doesn't lead to true parallel execution because of the GIL. A multi-threaded API performing CPU-bound work might actually be slower than a single-threaded one due to the overhead of thread switching.
        • I/O-bound tasks: The GIL is released when a thread is performing an I/O operation (e.g., network requests, file I/O, database queries). This means that for I/O-bound API tasks, multi-threading can offer concurrency because while one thread is waiting for an I/O operation to complete, another thread can acquire the GIL and execute Python bytecode.
      • How asyncio Helps:
        • asyncio is Python's library for writing concurrent code using the async/await syntax. It's built on a single-threaded, cooperative multitasking model (event loop).
        • Instead of threads, asyncio uses coroutines. When a coroutine encounters an awaitable operation (like an I/O request, e.g., fetching data from another API, waiting for a database query), it voluntarily yields control back to the event loop.
        • The event loop then switches to another ready coroutine, allowing it to run. This happens without releasing the GIL because the core execution is still single-threaded.
        • This model is highly efficient for I/O-bound concurrency because there's no thread switching overhead, and no contention for the GIL.
        • Analogy:
          • Threads (with GIL): Imagine a kitchen with one chef (CPU core) and multiple assistants (threads). Only one assistant can cook at a time because they all need to use the single stove (GIL). If one assistant needs to wait for water to boil, the stove is still occupied.
          • asyncio: One very efficient chef (event loop) who knows how to multitask. When the chef puts water on to boil, they immediately start chopping vegetables or preparing another dish instead of just waiting. When the water boils, they come back to it.
    • Question: When would you use async/await in a Python API, and what are the benefits? Provide a simple example of an asynchronous API endpoint.

    • Answer:

      • When to Use async/await in an API:

        • I/O-Bound Operations: This is the primary use case. If your API frequently makes calls to external services (other APIs, microservices), performs database queries, reads/writes files, or deals with network sockets, async/await can significantly improve performance and throughput.
        • Long-Polling/WebSockets: For real-time communication patterns where connections need to stay open and wait for events.
        • High Concurrency Requirements: When you expect a large number of concurrent requests, and each request involves I/O waiting, asyncio allows the server to handle many requests without blocking.
      • Benefits:

        • Increased Throughput: An asynchronous API can handle many more concurrent requests than a synchronous one, especially under I/O-bound workloads.
        • Better Resource Utilization: The server isn't wasting CPU cycles waiting for I/O operations to complete.
        • Scalability: Easier to scale for I/O-heavy applications.
        • Non-Blocking: API calls won't block the entire server process while waiting for slow external services.
      • Example (using FastAPI):

        Python
        from fastapi import FastAPI
        import httpx # An async HTTP client
        import asyncio # For sleep (simulating slow I/O)
        
        app = FastAPI()
        
        @app.get("/")
        async def read_root():
            return {"message": "Hello World"}
        
        @app.get("/sync_sleep")
        def sync_sleep():
            # This will block the entire server process for 5 seconds
            import time
            time.sleep(5)
            return {"message": "Slept synchronously for 5 seconds"}
        
        @app.get("/async_sleep")
        async def async_sleep():
            # This will yield control back to the event loop,
            # allowing other requests to be processed concurrently.
            await asyncio.sleep(5)
            return {"message": "Slept asynchronously for 5 seconds"}
        
        @app.get("/fetch_external_data")
        async def fetch_external_data():
            async with httpx.AsyncClient() as client:
                # Simulate fetching data from a slow external API
                response = await client.get("https://jsonplaceholder.typicode.com/todos/1")
                # Another slow operation
                await asyncio.sleep(2)
            return response.json()
        
        # To run this:
        # 1. pip install fastapi uvicorn httpx
        # 2. Save as main.py
        # 3. Run: uvicorn main:app --reload
        #
        # Test by opening multiple tabs for /sync_sleep vs /async_sleep
        # You'll notice /sync_sleep blocks, /async_sleep allows concurrency.
        

II. RESTful API Concepts

These questions focus on the architectural style commonly used for web APIs.

  1. REST Principles:

    • Question: What are the core principles of REST (Representational State Transfer)? Explain statelessness, client-server architecture, and uniform interface.

    • Answer:

      • REST (Representational State Transfer): An architectural style for designing networked applications. It's not a protocol, but a set of constraints that, when adhered to, create a web service that is stateless, scalable, and easy to maintain.
      • Core Principles (often summarized by the acronym "ROY FIELDING"):
        1. Client-Server: The client and server are separate and independent. The client is responsible for the user interface and user state, while the server is responsible for data storage and management. This separation allows for independent evolution of client and server.
        2. Stateless: Each request from client to server must contain all the information necessary to understand the request. The server8 should not store any client context between requests. This improves scalability and reliability.
        3. Cacheable: Responses from the server should explicitly or implicitly define themselves as cacheable or non-cacheable. This allows clients to cache responses, improving performance and reducing server9 load.
        4. Uniform Interface: This is the most crucial constraint. It simplifies the overall system architecture by ensuring a consistent way of interacting with resources. It has four sub-constraints:
          • Identification of Resources: Resources are identified by URIs (Uniform Resource Identifiers).
          • Manipulation of Resources Through Representations: Clients manipulate resources by sending representations of the resource (e.g., JSON, XML) to the server.
          • Self-Descriptive Messages: Each message includes enough information to describe how to process the message. (e.g., HTTP methods, content types).
          • Hypermedia as the Engine of Application State (HATEOAS): Resources should contain links to related resources, allowing clients to discover available actions and navigate the API.
        5. Layered System: A client cannot ordinarily tell whether it is connected directly to the end server, or to an intermediary along the10 way. This allows for scalability, load balancing, and security layers.
        6. Code-On-Demand (Optional): Servers can temporarily extend or customize client functionality by transferring executable code (e.g., JavaScript). This is11 the only optional constraint.
    • Question: How does REST relate to HTTP?

    • Answer: REST is an architectural style, while HTTP (Hypertext Transfer Protocol) is a protocol. HTTP is the most common and suitable protocol for implementing RESTful APIs because it naturally aligns with REST's principles:

      • Client-Server: HTTP inherently defines a client-server communication model.
      • Statelessness: HTTP is a stateless protocol; each request is independent.
      • Methods: HTTP verbs (GET, POST, PUT, DELETE, PATCH) map directly to CRUD (Create, Read, Update, Delete) operations on resources, which is central to REST.
      • URIs: HTTP uses URIs to identify resources.
      • Headers: HTTP headers (Content-Type, Accept, Authorization, Cache-Control) provide the self-descriptive nature required by REST.
      • Status Codes: HTTP status codes (200 OK, 404 Not Found, 500 Internal Server Error) provide standardized ways to communicate the result of a request. In essence, HTTP provides the foundational mechanisms that make building RESTful APIs practical and efficient.
  2. HTTP Methods (Verbs):

    • Question: Explain the common HTTP methods (GET, POST, PUT, PATCH, DELETE) and their typical use cases in a RESTful API.

    • Answer:

      • GET: READ data.
        • Use Case: Retrieve a resource or a collection of resources.
        • Characteristics: Idempotent, safe (no side effects on the server).
        • Example: GET /products, GET /products/123
      • POST: CREATE a new resource.
        • Use Case: Submit data to be processed to a specified resource, often resulting in the creation of a new resource.
        • Characteristics: Not idempotent (repeated requests create duplicate resources).
        • Example: POST /products (with product data in the request body to create a new product)
      • PUT: UPDATE/REPLACE an existing resource or CREATE if it doesn't exist.
        • Use Case: Replaces the entire resource at the specified URI with the data provided in the request body. If the resource does not exist, it may create it.
        • Characteristics: Idempotent (sending the same PUT request multiple times has the same effect as sending it once).
        • Example: PUT /products/123 (with the complete updated product data)
      • PATCH: PARTIAL UPDATE an existing resource.
        • Use Case: Apply partial modifications to a resource. Only the fields provided in the request body are updated.
        • Characteristics: Not necessarily idempotent (depends on the patch logic, but generally not if it's based on relative changes like "increment by X").
        • Example: PATCH /products/123 (with only the price field updated)
      • DELETE: DELETE a resource.
        • Use Case: Remove the resource identified by the URI.
        • Characteristics: Idempotent (deleting something that's already deleted has no further effect).
        • Example: DELETE /products/123
    • Question: What is idempotency, and which HTTP methods are considered idempotent? Why is this important?

    • Answer:

      • Idempotency: An operation is idempotent if executing it multiple times produces the same result as executing it once. In the context of APIs, it means that making the same request multiple times will not cause additional side effects on the server after the first successful execution.
      • Idempotent HTTP Methods:
        • GET
        • PUT
        • DELETE
        • HEAD
        • OPTIONS
      • Non-Idempotent HTTP Methods:
        • POST (sending the same POST request multiple times typically creates multiple resources).
        • PATCH (can be non-idempotent if the operation is relative, e.g., "add 5 to the quantity").
      • Importance:
        • Network Reliability: If a client sends a request and doesn't receive a response (due to network timeout, etc.), it can safely retry an idempotent request without worrying about unintended duplicate actions. This is crucial for handling flaky network conditions.
        • API Robustness: Simplifies error recovery for clients.
        • Easier Reasoning: It makes it easier to reason about the state of the system, as repeated identical requests won't unexpectedly alter data.
        • Caching: Idempotent methods (especially GET) are often easier to cache.
  3. Resources and URIs:

    • Question: What is a "resource" in the context of a RESTful API? How are resources identified?

    • Answer:

      • Resource: A resource is any information that can be named, stored, and manipulated. It's the core concept in REST. Resources can be a single entity (e.g., a specific user, a product), a collection of entities (e.g., all users, all products), a service (e.g., a search function), or even a relationship between entities (e.g., a user's orders). Resources are conceptual; their representation (e.g., JSON, XML) is what's transferred.
      • Identification: Resources are identified by URIs (Uniform Resource Identifiers). The URI uniquely points to a resource on the server.
        • Examples:
          • /users (collection of users)
          • /users/123 (a specific user with ID 123)
          • /products/electronics/laptops (a nested collection)
          • /orders/456/items (a sub-collection related to an order)
    • Question: Design a URI structure for a simple e-commerce API (e.g., for products, orders, users).

    • Answer:

      • General Principles:

        • Use nouns for resources.
        • Use plural nouns for collections, singular for specific items.
        • Use hierarchical paths to show relationships.
        • Avoid verbs in URIs (verbs are for HTTP methods).
        • Use kebab-case (-) for readability in paths.
      • Example Structure:

        • Products:
          • GET /products: Get all products (or paginate).
          • POST /products: Create a new product.
          • GET /products/{product_id}: Get a specific product.
          • PUT /products/{product_id}: Update a specific product (full replacement).
          • PATCH /products/{product_id}: Partially update a specific product.
          • DELETE /products/{product_id}: Delete a specific product.
          • GET /products?category=electronics&min_price=100: Filter products by query parameters.
        • Users:
          • GET /users: Get all users.
          • POST /users: Create a new user.
          • GET /users/{user_id}: Get a specific user.
          • PUT /users/{user_id}: Update a specific user.
          • DELETE /users/{user_id}: Delete a specific user.12
        • Orders (and nested resources):
          • GET /orders: Get all orders.
          • POST /orders: Create a new order.
          • GET /orders/{order_id}: Get a specific order.
          • PUT /orders/{order_id}: Update a specific order.
          • DELETE /orders/{order_id}: Delete a specific order.
          • GET /orders/{order_id}/items: Get items for a specific order.
          • GET /users/{user_id}/orders: Get all orders for a specific user.
          • POST /users/{user_id}/orders: Create an order for a specific user.
        • Shopping Cart:
          • GET /cart: Get the current user's shopping cart.
          • POST /cart/items: Add an item to the cart.
          • PUT /cart/items/{item_id}: Update an item in the cart (e.g., quantity).
          • DELETE /cart/items/{item_id}: Remove an item from the cart.
  4. HTTP Status Codes:

    • Question: Explain the purpose of HTTP status codes in API responses. Provide examples of 2xx, 4xx, and 5xx codes and their meanings.
    • Answer:
      • Purpose: HTTP status codes are 3-digit integer codes returned by a server in response to an HTTP request. They indicate the outcome of the request, providing a standardized way for clients to understand if their request was successful, if there was an error, and what type of error it was. This allows clients to react appropriately (e.g., retry, display an error message).
      • Categories:
        • 1xx Informational: Request received, continuing process. (Less common in typical API responses).
        • 2xx Success: The action was successfully received, understood, and accepted.
          • 200 OK: Standard success response. (e.g., GET request was successful).
          • 201 Created: The request has been fulfilled and resulted in a new resource being created. (e.g., POST request to create a user).
          • 204 No Content: The server successfully processed the request, but is not returning any content. (e.g., DELETE request successful, or a PUT request that only updates metadata).
        • 3xx Redirection: Further action needs to be taken by the user agent to fulfill the request.
          • 301 Moved Permanently: The resource has been permanently moved to a new URI.
          • 304 Not Modified: Used in caching; indicates that the resource has not been modified since the version specified by the client.
        • 4xx Client Error: The request contains bad syntax or cannot be fulfilled.
          • 400 Bad Request: General client error (malformed syntax, invalid parameters).
          • 401 Unauthorized: Authentication required or has failed.
          • 403 Forbidden: Authenticated, but lacks necessary permissions.
          • 404 Not Found: The requested resource could not be found.
          • 405 Method Not Allowed: The HTTP method used is not supported for the resource.
          • 409 Conflict: Request conflicts with current state of the resource (e.g., trying to create a user with an existing username).
          • 429 Too Many Requests: Rate limit exceeded.
        • 5xx Server Error: The server failed to fulfill an apparently valid request.
          • 500 Internal Server Error: A generic server-side error (unhandled exception).
          • 502 Bad Gateway: The server, while acting as a gateway or proxy, received an invalid response from an upstream server.
          • 503 Service Unavailable: The server is13 currently unable to handle the request due to temporary overloading or maintenance.14
  5. Headers:

    • Question: What is the role of HTTP headers in API requests and responses? Give examples of important headers (e.g., Content-Type, Accept, Authorization).
    • Answer:
      • Role of HTTP Headers: HTTP headers are key-value pairs that carry metadata about the request or response. They provide essential information that goes beyond the basic HTTP method and URI, enabling features like content negotiation, authentication, caching, and more.
      • Examples of Important Headers:
        • Content-Type (Request/Response):
          • Role: Indicates the media type of the body of the request or response. This tells the server (for requests) or client (for responses) how to parse the data.
          • Examples: application/json, application/xml, text/html, multipart/form-data.
          • Request: Content-Type: application/json (telling the server the body is JSON)
          • Response: Content-Type: application/json (telling the client the response body is JSON)
        • Accept (Request):
          • Role: Specifies the media types that the client is willing to accept in the response. This is part of content negotiation.
          • Examples: Accept: application/json, Accept: application/xml, Accept: */* (any).
        • Authorization (Request):
          • Role: Carries authentication credentials (e.g., tokens, API keys) from the client to the server.
          • Examples: Authorization: Bearer <token>, Authorization: Basic <base64-encoded-credentials>, Authorization: ApiKey <api_key>.
        • Cache-Control (Request/Response):
          • Role: Directs caching mechanisms in both requests and responses. Tells caches how long to store a resource, whether it can be re-used without revalidation, etc.
          • Examples: Cache-Control: no-cache, Cache-Control: max-age=3600, Cache-Control: public.
        • User-Agent (Request):
          • Role: Identifies the client software making the request (e.g., browser, custom script). Useful for logging, analytics, and debugging.
        • Location (Response):
          • Role: Used with 201 Created status code to indicate the URI of the newly created resource.
        • ETag / If-None-Match (Response / Request):
          • Role: Used for caching and conditional requests. ETag is a unique identifier for a specific version of a resource. If-None-Match is sent by the client to check if their cached version is still fresh.
        • Link (Response):
          • Role: Provides relations to other resources, crucial for HATEOAS (Hypermedia as the Engine of Application State).
          • Example: Link: <http://example.com/api/orders?page=2>; rel="next"
  6. Authentication and Authorization:

    • Question: What's the difference between authentication and authorization in an API? Describe common authentication methods for REST APIs (e.g., API keys, OAuth, JWT). How would you implement a simple API key authentication in a Python API?
    • Answer:
      • Authentication: The process of verifying who a user is. It answers the question: "Are you who you say you are?"

        • Example: Logging in with a username and password, providing an API key, presenting a valid token.
      • Authorization: The process of determining what an authenticated user is allowed to do. It answers the question: "Are you allowed to do that?"

        • Example: An admin user can delete resources, while a regular user can only view them. A user can only access their own profile data, not others'.
      • Common Authentication Methods:

        • API Keys:
          • How it works: A secret string (the API key) is generated and provided to the client. The client sends this key with each request, typically in a custom HTTP header (X-API-Key) or as a query parameter.
          • Pros: Simple to implement, easy to revoke.
          • Cons: Less secure if exposed (no expiration, often hardcoded), can be vulnerable if sent over unencrypted HTTP.
        • OAuth 2.0:
          • How it works: A complex protocol primarily used for delegated authorization. It allows a third-party application to access a user's resources on another service (e.g., "Login with Google"). Involves various "flows" (e.g., authorization code flow). Clients typically receive an "access token" after a user grants permission.
          • Pros: Secure delegation, widely adopted, suitable for single sign-on (SSO).
          • Cons: More complex to implement than API keys.
        • JSON Web Tokens (JWT):
          • How it works: After a user authenticates (e.g., with username/password), the server issues a digitally signed token (JWT). This token contains claims (e.g., user ID, roles, expiration time) and is sent by the client in the Authorization: Bearer <token> header with subsequent requests. The server verifies the signature to trust the token's content.
          • Pros: Stateless (server doesn't need to store session info), scalable, can contain arbitrary claims, signed for integrity.
          • Cons: Tokens cannot be revoked easily (unless a blocklist is implemented), tokens can be exposed if not handled securely (e.g., stored in local storage).
        • Basic Authentication: Sends username and password Base64 encoded in the Authorization header. Simple but insecure without HTTPS.
        • Session-based Authentication: Server creates a session for an authenticated user and sends a session ID (cookie) to the client. Client sends cookie with each request. Server looks up session data based on ID. State is maintained on the server.
      • Simple API Key Authentication in Python (Flask Example):

        Python
        from flask import Flask, request, jsonify
        
        app = Flask(__name__)
        
        # In a real app, this would be stored securely (e.g., database, environment variable)
        VALID_API_KEYS = {
            "your_secret_api_key_123",
            "another_client_key_abc"
        }
        
        def require_api_key(f):
            @app.route.wraps(f) # Important for preserving function metadata
            def decorated_function(*args, **kwargs):
                api_key = request.headers.get('X-API-Key')
                if not api_key or api_key not in VALID_API_KEYS:
                    return jsonify({"message": "Forbidden: Invalid or missing API Key"}), 403
                return f(*args, **kwargs)
            return decorated_function
        
        @app.route('/data')
        @require_api_key
        def get_protected_data():
            return jsonify({"message": "Access granted! This is protected data."})
        
        # To run:
        # app.run(debug=True)
        # Test with:
        # curl -H "X-API-Key: your_secret_api_key_123" http://127.0.0.1:5000/data
        # curl http://127.0.0.1:5000/data (will get 403)
        
  7. Versioning:

    • Question: Why is API versioning important? Describe different strategies for API versioning (e.g., URI versioning, header versioning).
    • Answer:
      • Why Important:
        • Backward Compatibility: Allows developers to make changes to an API without breaking existing clients.
        • Simultaneous Evolution: Enables new features and improvements to be rolled out without forcing all clients to update immediately.
        • Graceful Deprecation: Provides a pathway for clients to migrate to newer versions, giving ample time before older versions are retired.
        • Client Stability: Ensures existing client applications continue to function even as the API evolves.
      • Strategies:
        1. URI Versioning (Path Versioning):
          • How it works: Include the version number directly in the API path.
          • Examples: /v1/products, /api/v2/users
          • Pros: Very clear and explicit. Easy for clients to see and understand the version. Browsable (can just change URL in browser).
          • Cons: Pollutes the URI, requires changes to routing logic for every version. Not strictly RESTful (URI should identify the resource, not its version).
        2. Header Versioning:
          • How it works: The API version is specified in a custom HTTP request header.
          • Examples: X-API-Version: 1, Accept: application/vnd.myapi.v2+json (using a custom media type).
          • Pros: Keeps URIs clean. More flexible for content negotiation (can have different representations for the same resource).
          • Cons: Less discoverable for casual Browse. Requires clients to explicitly send the header.
        3. Query Parameter Versioning:
          • How it works: The version number is passed as a query parameter.
          • Examples: /products?version=1, /users?api_version=2
          • Pros: Simple to implement, easy to test in a browser.
          • Cons: Can be seen as less "clean" than path versioning for a fundamental aspect like version. If multiple query parameters are used, it can become messy.
        4. Content Negotiation / Accept Header Versioning (Media Type Versioning):
          • How it works: The client specifies the desired API version in the Accept header using a custom media type.
          • Example: Accept: application/vnd.mycompany.v1+json or Accept: application/json; version=1.0.
          • Pros: Most RESTful approach as it uses standard HTTP content negotiation.
          • Cons: More complex for clients to implement. Less intuitive for developers than path versioning. Not easily discoverable by just looking at the URL.
  8. Pagination:

    • Question: Why is pagination important for APIs that return large datasets? Describe different pagination strategies (e.g., offset-based, cursor-based).
    • Answer:
      • Why Important:
        • Performance: Prevents the server from spending excessive time and memory generating and sending huge responses.
        • Resource Conservation: Reduces network bandwidth usage for both client and server.
        • Client Usability: Large responses can be slow to download, parse, and display for clients. Pagination allows clients to fetch data in manageable chunks.
        • Scalability: Essential for APIs that handle growing amounts of data.
      • Pagination Strategies:
        1. Offset-Based Pagination (Page Number/Offset & Limit):
          • How it works: The client sends page_number (or offset) and page_size (or limit) parameters. The server skips offset records and returns limit records.
          • Example: GET /products?page=2&page_size=10 or GET /products?offset=10&limit=10
          • Pros: Simple to implement and understand. Easy to jump to specific pages (e.g., page 5).
          • Cons:
            • Performance Issues: As offset increases, the database might have to scan and discard a large number of rows, which can be slow for very large datasets.
            • Data Inconsistency (Drift): If items are added or deleted while a client is paginating, the results can "drift." For example, if an item on page 1 is deleted, page 2 might show an item that was previously on page 3, or skip an item entirely.
        2. Cursor-Based Pagination (Keyset Pagination / Seek Method):
          • How it works: Instead of page numbers, the client provides a "cursor" (usually an ID or a unique timestamp of the last item from the previous page) to indicate where the next page should start. The server returns items "after" or "before" that cursor.
          • Example: GET /products?since_id=12345&limit=10 or GET /products?last_modified_at=2023-10-26T10:00:00Z&limit=10
          • Pros:
            • Performance: Generally much faster for large datasets because it directly seeks to a point in the index, avoiding full table scans.
            • Consistency: More robust against data changes (additions/deletions) because it always continues from a known point.
          • Cons:
            • Cannot easily jump to an arbitrary page number.
            • More complex to implement.
            • Requires a stable sorting order and a unique, sequential column (like an ID or timestamp).
        3. Scroll/Token-Based Pagination (Less common for RESTful, more for search engines):
          • How it works: The server issues a "scroll ID" or token, which the client uses in subsequent requests to fetch the next batch of results. The server maintains state about the scroll.
          • Pros: Very efficient for iterating through extremely large datasets.
          • Cons: Not truly stateless (server maintains scroll state), often has an expiry.
  9. Rate Limiting:

    • Question: What is rate limiting in an API, and why is it necessary? How can it be implemented?
    • Answer:
      • What is Rate Limiting: Rate limiting is a strategy used by APIs to control the number of requests a user or client can make within a specified timeframe. If a client exceeds the defined limit, further requests are blocked or delayed, typically returning a 429 Too Many Requests HTTP status code.

      • Why Necessary:

        • Abuse Prevention: Prevents malicious activities like brute-force attacks, denial-of-service (DoS) attacks, or excessive scraping of data.
        • Resource Protection: Protects the API's backend resources (database, CPU, memory, network bandwidth) from being overwhelmed by a single client or a small group of clients.
        • Fair Usage: Ensures that all legitimate users have fair access to the API by preventing one user from monopolizing resources.
        • Cost Management: For cloud-based services, controlling API usage can directly impact infrastructure costs.
      • How it can be Implemented:

        1. Fixed Window Counter:
          • How it works: A counter is maintained for each client (identified by IP, API key, user ID, etc.) for a fixed time window (e.g., 60 seconds). Each request increments the counter. If the counter exceeds the limit within the window, requests are blocked.
          • Pros: Simple to implement.
          • Cons: Can suffer from "burst" problems at the window edges (e.g., a client makes all allowed requests at the very end of one window and then again at the very beginning of the next).
        2. Sliding Window Log:
          • How it works: For each client, a timestamp of every request is stored in a sorted list (e.g., in Redis). When a new request arrives, the server counts how many timestamps in the list fall within the current time window. If the count exceeds the limit, the request is denied. Old timestamps are pruned.
          • Pros: Very accurate, no "burst" problem.
          • Cons: More memory-intensive (stores all timestamps), computationally more expensive to count.
        3. Sliding Window Counter:
          • How it works: A hybrid approach. Divides the time into smaller fixed windows and keeps counts for each. For a new request, it calculates a weighted average of the current and previous window counts.
          • Pros: Better than fixed window, less memory-intensive than sliding window log.
          • Cons: Slightly more complex to implement than fixed window.
        4. Token Bucket Algorithm:
          • How it works: Imagine a bucket with a fixed capacity. Tokens are added to the bucket at a constant rate. Each request consumes one token. If the bucket is empty, the request is denied. If it's not empty, a token is removed, and the request proceeds.
          • Pros: Allows for bursts of traffic (up to bucket capacity) while smoothing out the overall rate.
          • Cons: More complex to implement.
        5. Leaky Bucket Algorithm:
          • How it works: Requests are added to a queue (the bucket). Requests are processed (leak out) at a constant rate. If the bucket is full, new requests are dropped.
          • Pros: Controls the rate of processing on the server side, provides a smoother output rate.
          • Cons: Introduces latency for requests if the queue fills up.
      • Implementation Details (Python/Flask Example with a library):

        Python
        from flask import Flask, jsonify, request
        from flask_limiter import Limiter
        from flask_limiter.util import get_remote_address
        
        app = Flask(__name__)
        
        # Configure Limiter to use Flask app and get IP address for key
        # In a real app, you might use a Redis backend for distributed limiting
        limiter = Limiter(
            get_remote_address,
            app=app,
            default_limits=["200 per day", "50 per hour"],
            storage_uri="memory://" # For simplicity; use "redis://localhost:6379" for production
        )
        
        @app.route("/unlimited")
        def unlimited_route():
            return jsonify({"message": "This route is not rate-limited."})
        
        @app.route("/limited")
        @limiter.limit("5 per minute") # Specific limit for this route
        def limited_route():
            return jsonify({"message": "This route is limited to 5 requests per minute."})
        
        @app.route("/user_specific_limited")
        @limiter.limit("2 per second", key_func=lambda: request.headers.get('X-User-ID', get_remote_address()))
        def user_specific_limited():
            user_id = request.headers.get('X-User-ID', 'Anonymous')
            return jsonify({"message": f"Hello {user_id}, this route is limited to 2 req/sec per user."})
        
        # Example: Apply default limits globally to all routes
        # @limiter.exempt  # Can exempt specific routes from default limits
        
        # To run:
        # pip install Flask Flask-Limiter
        # app.run(debug=True)
        # Test:
        # curl http://127.0.0.1:5000/limited (try more than 5 times in a minute)
        
  10. HATEOAS (Hypermedia as the Engine of Application State):

    • Question: What is HATEOAS, and how does it contribute to the discoverability and evolvability of a RESTful API?
    • Answer:
      • HATEOAS (Hypermedia as the Engine of Application State): It's a constraint of the REST architectural style that states that clients should interact with a RESTful API entirely through hypermedia provided dynamically by the server. Instead of knowing the API's URI structure beforehand, clients discover the available actions and next possible states by following links embedded in the representations they receive.

      • How it Works: API responses (e.g., JSON) include not just data but also hypermedia links that describe available actions or related resources. These links guide the client on what it can do next.

      • Contribution to Discoverability:

        • Self-Documentation: The API becomes self-documenting to a degree. A client doesn't need external documentation to know how to navigate from a product to its reviews or how to place_order for that product; the links are provided within the product's representation.
        • Dynamic Interaction: Clients can dynamically discover capabilities. For example, if a user resource has an "activate" link, the client knows the user can be activated. If that link is absent, the client knows it cannot.
      • Contribution to Evolvability:

        • Decoupling Client and Server: HATEOAS significantly decouples the client from the server's URI structure. If the server changes a URI path for a resource (e.g., from /v1/products to /v2/items), as long as the link is correctly updated in the representation, the client doesn't need to be updated. It just follows the new link.
        • Reduced Client Maintenance: Clients are less brittle to API changes. They don't hardcode URLs; they follow the provided links.
        • Easier API Evolution: API developers can change the underlying URI structure or add new functionality without breaking existing clients, as long as the links are correctly maintained in the hypermedia.
      • Example (JSON with HATEOAS links):

        JSON
        {
            "order_id": "ORD_12345",
            "status": "pending",
            "total_amount": 99.99,
            "customer_id": "USER_67890",
            "_links": {
                "self": {
                    "href": "/orders/ORD_12345",
                    "method": "GET",
                    "title": "Retrieve this order"
                },
                "customer": {
                    "href": "/users/USER_67890",
                    "method": "GET",
                    "title": "Retrieve customer details"
                },
                "cancel": {
                    "href": "/orders/ORD_12345/cancel",
                    "method": "POST",
                    "title": "Cancel this order",
                    "if_status": "pending" # Conditional link
                },
                "items": {
                    "href": "/orders/ORD_12345/items",
                    "method": "GET",
                    "title": "View order items"
                }
            }
        }
        

        In this example, the client receives the order details along with instructions (links) on what it can do next (cancel, view customer, view items). It doesn't need to hardcode /users/{user_id} or /orders/{order_id}/cancel.


III. Python API Frameworks and Libraries

These questions test your practical experience with popular Python tools for API development.

  1. Flask/Django REST Framework (DRF):

    • Question: Have you worked with Flask or Django REST Framework? Describe a project where you used one of these frameworks.

    • Answer: (This is a personal experience question, so a generic answer is provided as an example. You should replace this with your actual project experience.)

      • Example Answer (Flask): "Yes, I have significant experience with Flask for building lightweight, scalable REST APIs. In my previous role at [Company Name], I developed an internal service using Flask to manage and expose inventory data for our e-commerce platform.

        • Routes & Views: I defined routes for /products, /products/<id>, and /products/<id>/stock using Flask's @app.route decorator to handle GET, POST, PUT, and PATCH requests.
        • Request Handling: I used request.json to parse incoming JSON payloads for creating and updating product data.
        • Database Integration: The API interacted with a PostgreSQL database using SQLAlchemy. I implemented CRUD operations, ensuring data consistency and transactional integrity.
        • Error Handling: I set up custom error handlers (@app.errorhandler) to return appropriate HTTP status codes (e.g., 400 for bad input, 404 for not found, 500 for server errors) and consistent JSON error responses.
        • Authentication/Authorization: I integrated JWT-based authentication using PyJWT and implemented decorators to protect specific endpoints, checking user roles for authorization.
        • Testing: I wrote unit tests for API endpoints and business logic using pytest and Flask's test client to simulate HTTP requests.
        • Deployment: The application was containerized with Docker and deployed on Kubernetes."
      • Example Answer (Django REST Framework - DRF): "Yes, I've extensively used Django REST Framework for building robust and feature-rich REST APIs, particularly for larger applications requiring quick development cycles and built-in ORM features. For instance, in a project at [Company Name], I was part of a team that built an API for a content management system.

        • Serializers: DRF's ModelSerializer was invaluable for converting Django models to JSON representations and vice-versa, handling data validation and parsing automatically. I also created custom serializers for more complex data structures.
        • ViewSets & Routers: I leveraged ModelViewSet and DRF's default routers to quickly create standard CRUD endpoints for resources like articles, categories, and authors, significantly reducing boilerplate code.
        • Permissions & Authentication: I utilized DRF's built-in permission classes (e.g., IsAuthenticated, IsAdminUser) and customized them for role-based access control. For authentication, we used djangorestframework-simplejwt.
        • Filtering, Searching, Pagination: DRF's generic views and filters enabled easy implementation of search, filtering by various criteria, and different pagination styles (e.g., PageNumberPagination).
        • Custom Endpoints: For specific actions that didn't fit standard CRUD (e.g., publishing an article), I added custom actions to ViewSets.
        • Testing: We used Django's TestCase and DRF's APITestCase to write comprehensive tests for endpoints, serialization, and permissions."
    • Question: Explain how to define a basic API endpoint in Flask/DRF.

    • Answer:

      • Flask:
        Python
        from flask import Flask, jsonify, request
        
        app = Flask(__name__)
        
        @app.route('/hello', methods=['GET'])
        def hello_world():
            return jsonify({"message": "Hello, World!"})
        
        @app.route('/greet/<name>', methods=['GET'])
        def greet_user(name):
            return jsonify({"message": f"Hello, {name}!"})
        
        @app.route('/submit_data', methods=['POST'])
        def submit_data():
            data = request.json # Get JSON payload from request body
            if not data or 'value' not in data:
                return jsonify({"error": "Missing 'value' in request body"}), 400
            received_value = data['value']
            return jsonify({"status": "success", "received": received_value}), 200
        
        # To run:
        # app.run(debug=True)
        
      • Django REST Framework (DRF):
        • 1. Define a Django Model (e.g., myapp/models.py):
          Python
          from django.db import models
          
          class Product(models.Model):
              name = models.CharField(max_length=100)
              price = models.DecimalField(max_digits=10, decimal_places=2)
              stock = models.IntegerField(default=0)
          
              def __str__(self):
                  return self.name
          
        • 2. Create a Serializer (e.g., myapp/serializers.py):
          Python
          from rest_framework import serializers
          from .models import Product
          
          class ProductSerializer(serializers.ModelSerializer):
              class Meta:
                  model = Product
                  fields = ['id', 'name', 'price', 'stock']
          
        • 3. Define a View (e.g., myapp/views.py):
          Python
          from rest_framework import viewsets
          from .models import Product
          from .serializers import ProductSerializer
          
          class ProductViewSet(viewsets.ModelViewSet):
              queryset = Product.objects.all()
              serializer_class = ProductSerializer
          
        • 4. Register the ViewSet in urls.py (e.g., myproject/urls.py or myapp/urls.py):
          Python
          from django.contrib import admin
          from django.urls import path, include
          from rest_framework.routers import DefaultRouter
          from myapp.views import ProductViewSet # Assuming myapp is your app name
          
          router = DefaultRouter()
          router.register(r'products', ProductViewSet) # Registers CRUD for /products/ and /products/{id}/
          
          urlpatterns = [
              path('admin/', admin.site.urls),
              path('api/', include(router.urls)), # All API routes under /api/
          ]
          
        • Basic DRF endpoint (Function-based view for non-model data):
          Python
          from rest_framework.decorators import api_view
          from rest_framework.response import Response
          from rest_framework import status
          
          @api_view(['GET', 'POST'])
          def hello_drf(request):
              if request.method == 'GET':
                  return Response({"message": "Hello from DRF!"})
              elif request.method == 'POST':
                  data = request.data # Auto-parses JSON, form data etc.
                  if 'name' not in data:
                      return Response({"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST)
                  return Response({"message": f"Received name: {data['name']}"}, status=status.HTTP_201_CREATED)
          
          # In urls.py:
          # path('api/hello/', hello_drf),
          
    • Question: How do you handle routing and URL patterns in these frameworks?

    • Answer:

      • Flask:
        • Uses the @app.route() decorator to map a URL path to a Python function.
        • HTTP methods are specified in the methods argument (e.g., methods=['GET', 'POST']).
        • URL parts can be dynamic using angle brackets (<variable_name>), optionally with converters (<int:id>, <string:name>).
        Python
        from flask import Flask, request
        
        app = Flask(__name__)
        
        @app.route('/users/<int:user_id>', methods=['GET', 'PUT', 'DELETE'])
        def user_detail(user_id):
            if request.method == 'GET':
                return f"Getting user {user_id}"
            elif request.method == 'PUT':
                return f"Updating user {user_id}"
            elif request.method == 'DELETE':
                return f"Deleting user {user_id}"
        
      • Django REST Framework (DRF):
        • Builds upon Django's urlpatterns in urls.py.
        • path() or re_path(): Standard Django functions to map URLs to views.
        • Routers (DefaultRouter, SimpleRouter): DRF provides routers to automatically generate URL patterns for ViewSet classes. This is highly recommended for RESTful APIs as it reduces boilerplate.
          • A ViewSet combines the logic for a set of related views (list, detail, create, update, delete).
          • The router.register() method automatically creates URLs like /products/, /products/{id}/, and maps them to the appropriate list, retrieve, create, update, destroy methods of the ViewSet.
        Python
        # myapp/urls.py (within a Django app)
        from django.urls import path, include
        from rest_framework.routers import DefaultRouter
        from .views import ProductViewSet, hello_drf # From previous example
        
        router = DefaultRouter()
        router.register(r'products', ProductViewSet, basename='product') # basename is optional but good practice
        
        urlpatterns = [
            path('simple_hello/', hello_drf), # A simple function-based view
            path('', include(router.urls)), # Include all routes generated by the router
        ]
        
        # In your project's main urls.py:
        # path('api/', include('myapp.urls')),
        
    • Question: How do you serialize/deserialize data in DRF (e.g., using serializers.ModelSerializer)?

    • Answer:

      • Serialization: The process of converting complex data types (like Django model instances or querysets) into native Python data types that can then be easily rendered into JSON, XML, or other content types.
      • Deserialization: The process of converting incoming data (e.g., JSON from a request body) into native Python data types (like dictionaries) and then into Django model instances, typically performing validation along the way.
      • serializers.ModelSerializer:
        • Purpose: A powerful class in DRF that provides a shortcut for creating serializers that map directly to Django models. It automatically generates a set of fields based on the model and provides default implementations for create() and update() methods.

        • Example:

          Python
          # myapp/models.py
          from django.db import models
          
          class Book(models.Model):
              title = models.CharField(max_length=200)
              author = models.CharField(max_length=100)
              published_date = models.DateField()
              isbn = models.CharField(max_length=13, unique=True)
          
              def __str__(self):
                  return self.title
          
          # myapp/serializers.py
          from rest_framework import serializers
          from .models import Book
          
          class BookSerializer(serializers.ModelSerializer):
              class Meta:
                  model = Book
                  fields = ['id', 'title', 'author', 'published_date', 'isbn']
                  # You can also exclude fields: exclude = ['created_at']
                  # Or read-only fields: read_only_fields = ['isbn']
          
          # --- Usage Examples in a View ---
          # To serialize a single instance (for a GET /books/{id} response):
          # book = Book.objects.get(id=1)
          # serializer = BookSerializer(book)
          # print(serializer.data) # Output: {'id': 1, 'title': '...', 'author': '...', ...}
          
          # To serialize a queryset (for a GET /books/ response):
          # books = Book.objects.all()
          # serializer = BookSerializer(books, many=True) # many=True for a list of objects
          # print(serializer.data) # Output: [{'id': 1, ...}, {'id': 2, ...}]
          
          # To deserialize and validate incoming data (for a POST /books/ request):
          # data = {'title': 'New Book', 'author': 'John Doe', 'published_date': '2023-01-01', 'isbn': '1234567890123'}
          # serializer = BookSerializer(data=data)
          # if serializer.is_valid():
          #     book_instance = serializer.save() # Creates a new Book instance in the database
          #     print(book_instance.title)
          # else:
          #     print(serializer.errors) # Validation errors
          
          # To deserialize and update an existing instance (for a PUT/PATCH /books/{id} request):
          # book = Book.objects.get(id=1)
          # update_data = {'price': 25.00} # or {'title': 'Updated Title'}
          # serializer = BookSerializer(book, data=update_data, partial=True) # partial=True for PATCH
          # if serializer.is_valid():
          #     updated_book = serializer.save() # Updates the existing Book instance
          # else:
          #     print(serializer.errors)
          
      • serializers.Serializer (Custom/Non-Model Data):
        • For data that doesn't directly map to a Django model, or for complex nested structures. You define fields explicitly.
        Python
        # myapp/serializers.py
        class ContactFormSerializer(serializers.Serializer):
            name = serializers.CharField(max_length=100)
            email = serializers.EmailField()
            message = serializers.CharField(style={'base_template': 'textarea.html'})
        
            def create(self, validated_data):
                # Custom logic for saving/processing the contact form data
                print(f"Contact form received from {validated_data['name']} ({validated_data['email']}): {validated_data['message']}")
                return validated_data # Return data or a dummy object
        
            def update(self, instance, validated_data):
                pass # Not applicable for a contact form submission
        
  2. Requests Library:

    • Question: How do you make an HTTP GET/POST request to an external API using Python's requests library?

    • Answer:

      • The requests library is a de facto standard for making HTTP requests in Python due to its user-friendliness and comprehensive features.

      • GET Request:

        Python
        import requests
        
        url = "https://jsonplaceholder.typicode.com/todos/1"
        
        try:
            response = requests.get(url)
            response.raise_for_status()  # Raise an exception for HTTP errors (4xx or 5xx)
        
            data = response.json()  # Parse JSON response body
            print("GET Request Successful:")
            print(f"Status Code: {response.status_code}")
            print(f"Response Data: {data}")
        
        except requests.exceptions.HTTPError as errh:
            print(f"HTTP Error: {errh}")
        except requests.exceptions.ConnectionError as errc:
            print(f"Error Connecting: {errc}")
        except requests.exceptions.Timeout as errt:
            print(f"Timeout Error: {errt}")
        except requests.exceptions.RequestException as err:
            print(f"Something went wrong: {err}")
        
      • POST15 Request:

        Python
        import requests
        import json
        
        url = "https://jsonplaceholder.typicode.com/posts"
        payload = {
            "title": "foo",
            "body": "bar",
            "userId": 1
        }
        
        try:
            # For JSON payload, use the 'json' parameter. requests will set Content-Type header automatically.
            response = requests.post(url, json=payload)
            response.raise_for_status()
        
            created_post = response.json()
            print("\nPOST Request Successful:")
            print(f"Status Code: {response.status_code}")
            print(f"Created Post: {created_post}")
        
        except requests.exceptions.RequestException as e:
            print(f"POST Request Error: {e}")
        
    • Question: How do you handle JSON responses from an API using requests?

    • Answer:

      • The requests library provides a convenient json() method on the response object. This method attempts to parse the response body as JSON and returns it as a Python dictionary or list.
      • It automatically handles the Content-Type: application/json header. If the response is not valid JSON, it will raise a json.JSONDecodeError.
      Python
      import requests
      
      url = "https://api.github.com/users/octocat" # Example API returning JSON
      
      try:
          response = requests.get(url)
          response.raise_for_status() # Check for HTTP errors
      
          # Safely parse JSON response
          if response.headers['Content-Type'].startswith('application/json'):
              data = response.json()
              print("Parsed JSON data:")
              print(data)
              print(f"User name: {data.get('name')}")
              print(f"Followers: {data.get('followers')}")
          else:
              print("Response is not JSON. Content-Type:", response.headers['Content-Type'])
              print("Response text:", response.text)
      
      except requests.exceptions.RequestException as e:
          print(f"Error fetching data: {e}")
      except ValueError as e: # Catch JSON decoding errors specifically
          print(f"Error decoding JSON: {e}")
      
    • Question: How would you send custom headers or query parameters with a requests call?

    • Answer:

      • Custom Headers: Use the headers parameter, which expects a dictionary of header names and values.

        Python
        import requests
        
        url = "https://api.example.com/data"
        custom_headers = {
            "User-Agent": "MyPythonApp/1.0",
            "X-API-Key": "your_secret_key_here",
            "Accept-Language": "en-US,en;q=0.9"
        }
        
        try:
            response = requests.get(url, headers=custom_headers)
            response.raise_for_status()
            print("Response with custom headers:", response.json())
        except requests.exceptions.RequestException as e:
            print(f"Error: {e}")
        
      • Query Parameters: Use the params parameter, which expects a dictionary or a list of tuples. requests will automatically encode these into the URL's query string.

        Python
        import requests
        
        base_url = "https://api.example.com/search"
        query_params = {
            "q": "python api",
            "category": "programming",
            "page": 1,
            "sort_by": "relevance"
        }
        
        try:
            # The URL will become: https://api.example.com/search?q=python+api&category=programming&page=1&sort_by=relevance
            response = requests.get(base_url, params=query_params)
            response.raise_for_status()
            print("Response with query parameters:", response.url) # Show the full URL
            print(response.json())
        except requests.exceptions.RequestException as e:
            print(f"Error: {e}")
        
  3. Testing:

    • Question: How would you unit test an API endpoint in Python? What tools would you use (e.g., unittest, pytest)?

    • Answer:

      • Unit Testing an API Endpoint: The goal is to test the individual function or method that handles the API request logic in isolation from external dependencies (like the actual HTTP server, database, or external APIs). We simulate the incoming request and check the returned response.

      • Tools:

        • unittest (Python's built-in testing framework): Suitable for basic testing.
        • pytest (Popular third-party framework): Highly recommended for its simplicity, powerful features (fixtures, parameterized tests, rich assertion failures), and extensive plugin ecosystem.
        • Framework-specific Test Clients: Flask, Django, FastAPI (via httpx) all provide test clients that simulate HTTP requests without actually starting a server. This is crucial for efficient unit/integration testing of API endpoints.
        • Mocking Libraries: unittest.mock (built-in), Mocker (Pytest plugin). Used to replace external dependencies (database calls, external API calls) with controlled mock objects so that the test focuses only on the API endpoint's logic.
      • Example (Flask API Endpoint with pytest and Flask's Test Client):

        Python
        # app.py (Your Flask application)
        from flask import Flask, jsonify, request
        
        app = Flask(__name__)
        
        # In a real app, this might interact with a database or service
        def get_user_data(user_id):
            if user_id == 1:
                return {"id": 1, "name": "Alice", "email": "alice@example.com"}
            return None
        
        def create_user_data(name, email):
            # Simulate adding to DB
            new_user_id = 2 # Dummy ID
            return {"id": new_user_id, "name": name, "email": email}
        
        @app.route('/users/<int:user_id>', methods=['GET'])
        def get_user(user_id):
            user = get_user_data(user_id)
            if user:
                return jsonify(user), 200
            return jsonify({"message": "User not found"}), 404
        
        @app.route('/users', methods=['POST'])
        def create_user():
            data = request.get_json()
            if not data or 'name' not in data or 'email' not in data:
                return jsonify({"message": "Name and email are required"}), 400
        
            new_user = create_user_data(data['name'], data['email'])
            return jsonify(new_user), 201
        
        # test_app.py (Your pytest tests)
        import pytest
        from app import app, get_user_data, create_user_data # Import app and helper functions
        
        @pytest.fixture
        def client():
            # Configure the Flask app for testing
            app.config['TESTING'] = True
            with app.test_client() as client:
                yield client # This client is used to make requests to the app
        
        def test_get_existing_user(client, mocker):
            # Mock the external dependency (get_user_data) to control its behavior
            mocker.patch('app.get_user_data', return_value={"id": 1, "name": "TestUser", "email": "test@example.com"})
        
            response = client.get('/users/1') # Simulate GET request
            assert response.status_code == 200
            assert response.json == {"id": 1, "name": "TestUser", "email": "test@example.com"}
        
        def test_get_non_existing_user(client, mocker):
            mocker.patch('app.get_user_data', return_value=None)
        
            response = client.get('/users/999')
            assert response.status_code == 404
            assert response.json == {"message": "User not found"}
        
        def test_create_user_success(client, mocker):
            mocker.patch('app.create_user_data', return_value={"id": 2, "name": "New User", "email": "new@example.com"})
        
            # Simulate POST request with JSON data
            response = client.post(
                '/users',
                json={"name": "New User", "email": "new@example.com"},
                content_type='application/json'
            )
            assert response.status_code == 201
            assert response.json == {"id": 2, "name": "New User", "email": "new@example.com"}
        
        def test_create_user_missing_data(client):
            response = client.post(
                '/users',
                json={"name": "Only Name"}, # Missing email
                content_type='application/json'
            )
            assert response.status_code == 400
            assert response.json == {"message": "Name and email are required"}
        
        • To run these tests:
          1. pip install Flask pytest pytest-mock
          2. Save app.py and test_app.py in the same directory.
          3. Run pytest in your terminal from that directory.
    • Question: What is the difference between unit testing and integration testing for APIs?

    • Answer:

      • Unit Testing:
        • Focus: Tests the smallest testable parts of an application (units), typically individual functions, methods, or classes, in isolation.
        • Scope: Verifies that each unit performs its specific task correctly.
        • Dependencies: External dependencies (databases, file systems, other APIs, network) are typically mocked or stubbed out. This means replacing real dependencies with controlled, simulated objects.
        • Goal: To ensure the internal logic of a component is sound, irrespective of how other components behave. Fast execution.
        • API Context: Testing a single API endpoint handler function, making sure it processes input correctly, calls internal helper functions as expected, and formats its output, without actually hitting a database or sending an HTTP request over the network.
      • Integration Testing:
        • Focus: Tests how different units or modules of an application work together as a group.
        • Scope: Verifies the interactions between components and ensures they integrate correctly to achieve a larger functionality.
        • Dependencies: Real dependencies are often used. For an API, this might mean connecting to a test database, or making calls to other internal services. External third-party APIs might still be mocked, but the API's own components (e.g., the web server, routing, database ORM, actual database) are connected.
        • Goal: To uncover defects that arise from the interaction between integrated units. Slower than unit tests.
        • API Context: Making a real HTTP request to a running API endpoint (even if it's running in a test environment), checking if the request correctly hits the right route, interacts with the database (or other internal services), processes data, and returns the expected HTTP status code and response body. This verifies the "flow" through multiple layers of the API.
      • Analogy:
        • Unit Test: Testing if a car engine's spark plugs generate a spark, the fuel pump delivers fuel, and the pistons move correctly, all in isolation.
        • Integration Test: Testing if the entire car engine starts and runs when you turn the key, verifying that all those individual components work together.

 

No comments:

Post a Comment