Designing REST APIs within a monolith isn’t inherently limiting; it’s often the most pragmatic starting point for many applications.
Let’s say you’re building an e-commerce platform. Your monolith handles everything: user accounts, product catalog, order processing, and payment gateway integration. To expose this functionality externally, you’ll need APIs. A RESTful approach is common.
Consider a GET /products endpoint. Your monolith might look something like this (simplified Python Flask):
from flask import Flask, jsonify, request
app = Flask(__name__)
# In-memory data for demonstration
products_db = {
"prod_123": {"name": "Wireless Mouse", "price": 25.99, "stock": 150},
"prod_456": {"name": "Mechanical Keyboard", "price": 75.00, "stock": 75},
"prod_789": {"name": "Webcam", "price": 49.99, "stock": 200},
}
@app.route('/products', methods=['GET'])
def get_products():
# Basic filtering by query parameter
min_price = request.args.get('min_price', type=float)
max_price = request.args.get('max_price', type=float)
filtered_products = []
for prod_id, product_data in products_db.items():
if min_price is not None and product_data['price'] < min_price:
continue
if max_price is not None and product_data['price'] > max_price:
continue
filtered_products.append({"id": prod_id, **product_data})
return jsonify(filtered_products)
@app.route('/products/<string:product_id>', methods=['GET'])
def get_product(product_id):
product_data = products_db.get(product_id)
if product_data:
return jsonify({"id": product_id, **product_data})
else:
return jsonify({"error": "Product not found"}), 404
if __name__ == '__main__':
app.run(debug=True, port=5000)
When a client makes a GET http://localhost:5000/products?min_price=50.00 request, the Flask application receives it. The get_products function is invoked. It iterates through products_db, checks the min_price argument, and returns a JSON list of products meeting the criteria. The GET http://localhost:5000/products/prod_123 request follows a similar path, but the get_product function retrieves a single item by its ID.
The core problem this approach solves is exposing business logic to external consumers without needing complex distributed systems from day one. You get a single codebase, a single deployment artifact, and a straightforward development experience.
Internally, your monolith’s API endpoints are just functions or methods within your application’s code. They receive HTTP requests, parse them (headers, query parameters, body), interact with your internal data stores or business logic layers, and construct HTTP responses.
The levers you control are standard web framework features: routing (mapping URLs to functions), request parsing, response serialization (usually JSON), and HTTP status codes. For example, in the code above, @app.route('/products/<string:product_id>', methods=['GET']) is the routing mechanism, and jsonify handles the JSON serialization. Returning 404 signals "Not Found" to the client.
The surprising thing about monolith APIs is how much complexity can be hidden behind a simple interface. A single POST /orders request might trigger a cascade of internal operations: validating inventory, charging a credit card via an external service, updating a database, sending an email notification, and even queuing a background job for fulfillment. All of this happens within the same process, often without the API handler itself being aware of the full chain. The "API" is just the entry point.
When you start experiencing performance issues with a monolith API, it’s often not the API definition itself, but the internal implementation that’s the bottleneck. For instance, if GET /products becomes slow, it’s likely because the products_db is massive and the linear scan in get_products is taking too long, or the database queries behind it are inefficient. You’d then optimize the data retrieval, perhaps by adding indexes or caching.
As your application grows, you’ll naturally encounter the need to manage different API versions. This is typically handled by prefixing your routes, for example, /v1/products and /v2/products. The monolith would then contain multiple implementations for the same conceptual resource, each mapped to a different versioned route.
The next hurdle will be managing the complexity of a rapidly growing API surface within a single codebase, which often leads to discussions about API gateways or breaking the monolith into microservices.