APIs vs. Endpoints: Breaking Down the Differences
Despite being basic ideas in web development and programming, APIs and endpoints can lead to misunderstanding. These phrases have different meanings and purposes, despite their close relationship and sometimes interchange, which leads to misunderstanding. This blog article will go over APIs and endpoints in detail, explain how they vary, and provide you many of code samples to help you understand.
Introduction to APIs
Application Programming Interface is referred to as API. It is a system of guidelines and procedures that enables various software programs to speak with one another. Through endpoints, an API exposes data or functionality and specifies the proper method by which a developer may request services from an operating system or other applications.
APIs are used in various contexts:
Web APIs: Enable communication between a web server and a client.
Operating System APIs: Allow applications to use resources of the operating system.
Library APIs: Provide access to the functions of software libraries.
Example: Web API
Consider a simple web API that provides data about books. This API could allow clients to fetch a list of books, add a new book, update book details, or delete a book.
# Example of a simple Web API using Flask (Python)
from flask import Flask, jsonify, request
app = Flask(__name__)
books = [
{'id': 1, 'title': '1984', 'author': 'George Orwell'},
{'id': 2, 'title': 'To Kill a Mockingbird', 'author': 'Harper Lee'}
]
@app.route('/books', methods=['GET'])
def get_books():
return jsonify(books)
@app.route('/books', methods=['POST'])
def add_book():
new_book = request.get_json()
books.append(new_book)
return jsonify(new_book), 201
@app.route('/books/<int:id>', methods=['PUT'])
def update_book(id):
book = next((b for b in books if b['id'] == id), None)
if book is None:
return jsonify({'error': 'Book not found'}), 404
data = request.get_json()
book.update(data)
return jsonify(book)
@app.route('/books/<int:id>', methods=['DELETE'])
def delete_book(id):
global books
books = [b for b in books if b['id'] != id]
return '', 204
if __name__ == '__main__':
app.run(debug=True)
In this example, the API defines several endpoints (routes) for managing books. Each endpoint allows clients to perform specific operations on the books resource.
What are Endpoints?
An endpoint is a specific URL pattern at which a particular service is made available. It represents one end of a communication channel, often corresponding to an operation exposed by the API.
Endpoints are crucial components of APIs. They define where and how resources can be accessed or manipulated.
Example: Endpoints in a Web API
Continuing with the previous example, let's identify the endpoints:
G
ET /books
: Fetches a list of all books.PUT /books/<int:id>
: Updates the book with the specified ID.DEL
ETE
/
books/<int:id>
: Deletes the book with the specified ID.
Each of these endpoints performs a different operation, allowing clients to interact with the books resource in specific ways.
Key Differences Between APIs and Endpoints
Scope:
Functionality:
Structure:
Detailed Code Examples
To further illustrate the differences and interactions between APIs and endpoints, let's expand our previous example. We will add more functionalities and show how endpoints work within the broader context of an API.
Adding Authentication
Let's add authentication to our API. Only authenticated users should be able to add, update, or delete books.
from flask import Flask, jsonify, request, abort
app = Flask(__name__)
books = [
{'id': 1, 'title': '1984', 'author': 'George Orwell'},
{'id': 2, 'title': 'To Kill a Mockingbird', 'author': 'Harper Lee'}
]
users = {
'user1': 'password1',
'user2': 'password2'
}
def authenticate():
auth = request.authorization
if not auth or not users.get(auth.username) == auth.password:
return False
return True
@app.route('/books', methods=['GET'])
def get_books():
return jsonify(books)
@app.route('/books', methods=['POST'])
def add_book():
if not authenticate():
return jsonify({'error': 'Unauthorized access'}), 401
new_book = request.get_json()
books.append(new_book)
return jsonify(new_book), 201
@app.route('/books/<int:id>', methods=['PUT'])
def update_book(id):
if not authenticate():
return jsonify({'error': 'Unauthorized access'}), 401
book = next((b for b in books if b['id'] == id), None)
if book is None:
return jsonify({'error': 'Book not found'}), 404
data = request.get_json()
book.update(data)
return jsonify(book)
@app.route('/books/<int:id>', methods=['DELETE'])
def delete_book(id):
if not authenticate():
return jsonify({'error': 'Unauthorized access'}), 401
global books
books = [b for b in books if b['id'] != id]
return '', 204
if __name__ == '__main__':
app.run(debug=True)
In this example, the authenticate
function checks if the request contains valid authentication credentials. The POST
, PUT
, and DELETE
endpoints are protected, requiring valid credentials to access.
Adding Error Handling
Let's improve our API by adding more detailed error handling. This ensures that clients receive meaningful error messages when something goes wrong.
from flask import Flask, jsonify, request, abort
app = Flask(__name__)
books = [
{'id': 1, 'title': '1984', 'author': 'George Orwell'},
{'id': 2, 'title': 'To Kill a Mockingbird', 'author': 'Harper Lee'}
]
users = {
'user1': 'password1',
'user2': 'password2'
}
def authenticate():
auth = request.authorization
if not auth or not users.get(auth.username) == auth.password:
return False
return True
@app.errorhandler(400)
def bad_request(error):
return jsonify({'error': 'Bad request'}), 400
@app.errorhandler(401)
def unauthorized(error):
return jsonify({'error': 'Unauthorized access'}), 401
@app.errorhandler(404)
def not_found(error):
return jsonify({'error': 'Resource not found'}), 404
@app.route('/books', methods=['GET'])
def get_books():
return jsonify(books)
@app.route('/books', methods=['POST'])
def add_book():
if not authenticate():
abort(401)
if not request.json or not 'title' in request.json:
abort(400)
new_book = {
'id': books[-1]['id'] + 1 if books else 1,
'title': request.json['title'],
'author': request.json.get('author', "")
}
books.append(new_book)
return jsonify(new_book), 201
@app.route('/books/<int:id>', methods=['PUT'])
def update_book(id):
if not authenticate():
abort(401)
book = next((b for b in books if b['id'] == id), None)
if book is None:
abort(404)
if not request.json:
abort(400)
book['title'] = request.json.get('title', book['title'])
book['author'] = request.json.get('author', book['author'])
return jsonify(book)
@app.route('/books/<int:id>', methods=['DELETE'])
def delete_book(id):
if not authenticate():
abort(401)
global books
books = [b for b in books if b['id'] != id]
return '', 204
if __name__ == '__main__':
app.run(debug=True)
In this improved version, we added custom error handlers for different HTTP status codes. The abort
function is used to trigger these error handlers when necessary, providing more informative error messages to clients.
Advanced Concepts: Versioning and Rate Limiting
As APIs grow in complexity, it becomes essential to manage different versions and limit the rate of requests to ensure stability and backward compatibility.
API Versioning
API versioning allows you to maintain different versions of your API to support legacy clients while adding new features for newer clients. Let's add versioning to our API.
from flask import Flask, jsonify, request, abort
app = Flask(__name__)
books_v1 = [
{'id': 1, 'title': '1984', 'author': 'George Orwell'},
{'
id': 2, 'title': 'To Kill a Mockingbird', 'author': 'Harper Lee'}
]
books_v2 = [
{'id': 1, 'title': '1984', 'author': 'George Orwell', 'published': '1949'},
{'id': 2, 'title': 'To Kill a Mockingbird', 'author': 'Harper Lee', 'published': '1960'}
]
users = {
'user1': 'password1',
'user2': 'password2'
}
def authenticate():
auth = request.authorization
if not auth or not users.get(auth.username) == auth.password:
return False
return True
@app.route('/v1/books', methods=['GET'])
def get_books_v1():
return jsonify(books_v1)
@app.route('/v1/books', methods=['POST'])
def add_book_v1():
if not authenticate():
abort(401)
if not request.json or not 'title' in request.json:
abort(400)
new_book = {
'id': books_v1[-1]['id'] + 1 if books_v1 else 1,
'title': request.json['title'],
'author': request.json.get('author', "")
}
books_v1.append(new_book)
return jsonify(new_book), 201
@app.route('/v1/books/<int:id>', methods=['PUT'])
def update_book_v1(id):
if not authenticate():
abort(401)
book = next((b for b in books_v1 if b['id'] == id), None)
if book is None:
abort(404)
if not request.json:
abort(400)
book['title'] = request.json.get('title', book['title'])
book['author'] = request.json.get('author', book['author'])
return jsonify(book)
@app.route('/v1/books/<int:id>', methods=['DELETE'])
def delete_book_v1(id):
if not authenticate():
abort(401)
global books_v1
books_v1 = [b for b in books_v1 if b['id'] != id]
return '', 204
@app.route('/v2/books', methods=['GET'])
def get_books_v2():
return jsonify(books_v2)
@app.route('/v2/books', methods=['POST'])
def add_book_v2():
if not authenticate():
abort(401)
if not request.json or not 'title' in request.json:
abort(400)
new_book = {
'id': books_v2[-1]['id'] + 1 if books_v2 else 1,
'title': request.json['title'],
'author': request.json.get('author', ""),
'published': request.json.get('published', "")
}
books_v2.append(new_book)
return jsonify(new_book), 201
@app.route('/v2/books/<int:id>', methods=['PUT'])
def update_book_v2(id):
if not authenticate():
abort(401)
book = next((b for b in books_v2 if b['id'] == id), None)
if book is None:
abort(404)
if not request.json:
abort(400)
book['title'] = request.json.get('title', book['title'])
book['author'] = request.json.get('author', book['author'])
book['published'] = request.json.get('published', book['published'])
return jsonify(book)
@app.route('/v2/books/<int:id>', methods=['DELETE'])
def delete_book_v2(id):
if not authenticate():
abort(401)
global books_v2
books_v2 = [b for b in books_v2 if b['id'] != id]
return '', 204
if __name__ == '__main__':
app.run(debug=True)
In this example, we created two versions of the API (v1
and v2
). Each version has its own set of endpoints, allowing clients to choose which version to interact with. This approach helps maintain backward compatibility while enabling the introduction of new features and improvements.
Rate Limiting
Rate restriction sets a cap on how many requests a client may make to an API in a certain amount of time. Fair usage among clients is ensured and abuse is prevented. Let's use the flask-limiter extension to provide rate restriction for our API.
from flask import Flask, jsonify, request, abort
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
app = Flask(__name__)
limiter = Limiter(
get_remote_address,
app=app,
default_limits=["200 per day", "50 per hour"]
)
books = [
{'id': 1, 'title': '1984', 'author': 'George Orwell'},
{'id': 2, 'title': 'To Kill a Mockingbird', 'author': 'Harper Lee'}
]
users = {
'user1': 'password1',
'user2': 'password2'
}
def authenticate():
auth = request.authorization
if not auth or not users.get(auth.username) == auth.password:
return False
return True
@app.errorhandler(400)
def bad_request(error):
return jsonify({'error': 'Bad request'}), 400
@app.errorhandler(401)
def unauthorized(error):
return jsonify({'error': 'Unauthorized access'}), 401
@app.errorhandler(404)
def not_found(error):
return jsonify({'error': 'Resource not found'}), 404
@app.errorhandler(429)
def ratelimit_error(error):
return jsonify({'error': 'Too many requests'}), 429
@app.route('/books', methods=['GET'])
@limiter.limit("10 per minute")
def get_books():
return jsonify(books)
@app.route('/books', methods=['POST'])
@limiter.limit("5 per minute")
def add_book():
if not authenticate():
abort(401)
if not request.json or not 'title' in request.json:
abort(400)
new_book = {
'id': books[-1]['id'] + 1 if books else 1,
'title': request.json['title'],
'author': request.json.get('author', "")
}
books.append(new_book)
return jsonify(new_book), 201
@app.route('/books/<int:id>', methods=['PUT'])
@limiter.limit("5 per minute")
def update_book(id):
if not authenticate():
abort(401)
book = next((b for b in books if b['id'] == id), None)
if book is None:
abort(404)
if not request.json:
abort(400)
book['title'] = request.json.get('title', book['title'])
book['author'] = request.json.get('author', book['author'])
return jsonify(book)
@app.route('/books/<int:id>', methods=['DELETE'])
@limiter.limit("5 per minute")
def delete_book(id):
if not authenticate():
abort(401)
global books
books = [b for b in books if b['id'] != id]
return '', 204
if __name__ == '__main__':
app.run(debug=True)
In this example, the flask-limiter
extension is used to apply rate limits to different endpoints. The default_limits
parameter sets global rate limits, while the @limiter.limit
decorator applies specific rate limits to individual endpoints. If a client exceeds the allowed number of requests, they receive a 429 Too Many Requests
error.
Best Practices for Designing APIs and Endpoints
Consistency:
Versioning:
Documentation:
Error Handling:
Security:
Rate Limiting:
Testing:
Monitoring:
Conclusion
APIs and endpoints are fundamental concepts in web development, each playing a distinct role in enabling communication between software applications. Understanding the differences between APIs and endpoints is crucial for designing robust and efficient systems.
In this blog post, we explored the concepts of APIs and endpoints, highlighted their differences, and provided detailed code examples to illustrate their usage. We also discussed advanced topics like versioning and rate limiting, along with best practices for designing APIs.
By following these guidelines and best practices, you can create APIs that are not only functional but also secure, scalable, and easy to use. Whether you are building a simple application or a complex system, a solid understanding of APIs and endpoints will help you deliver high-quality software solutions.
Subscribe to my newsletter
Read articles from Nile Bits directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Nile Bits
Nile Bits
Nile Bits is a software company, focusing on outsourcing software development and custom software solutions. Our outsourcing software services and solutions are designed with a focus on secure, scalable, expandable and reliable business systems. Via our low cost, high quality and reliable outsourcing software services, we provide to our clients value for money and therefore client satisfaction.