Generate APIs in Flask without coding - Open-Source Project
Hello! This article presents an open-source tool able to generate secure APIs using Flask as backend technology. Soft UI Dashboard, the starter that incorporates the generator, is published on GitHub and based on the permissive (MIT) license, can be used in commercial projects or eLearning activities. Thanks for reading!
- 👉 Soft UI Dashboard Flask - source code
- 👉 Soft UI Dashboard Flask - product page
A video material that demonstrates the process can be found on YouTube (link below). Here is the transcript of the presentation:
- ✅ Download the project using GIT
- ✅ Start in Docker (the API is empty)
- ✅ Define a
Books
model and generate the API - ✅ Access and interact with the API (CRUD calls)
- ✅ Register a new model
Cities
- ✅ Re-generate the API using the CLI
- ✅ Access and use the new API identity
✨ How it works
The Generator is built using a design pattern that separates the common part like authorization and helpers from the opinionated part that is tied to the model(s) definitions. The functional components of the tool are listed below:
- CLI command that launches the tool:
flask gen_api
- Models parser: that loads the definitions
- Configuration loader: API_GENERATOR section
- Helpers like token_required, that check the permissions during runtime
- The core generator that uses all the above info and builds API
To keep things as simple as possible, the flow is always one way, with the API service completely re-generated at each iteration.
For curious minds, the main parts of the tool are explained with a verbose presentation in the next sections. However, to understand in full the product, reverse engineering on the source code might be required.
👉 #1 - API Generator Input
The API service is generated based on the following input:
The model's definition is isolated in "apps/models.py" file:
# apps/models.py - Truncated content
from apps import db
class Book(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(64))
The API_GENERATOR section specifies the models to be processed by the generator
API_GENERATOR = {
"books": "Book",
}
The above definitions inform the tool to generate the API for the Book
model.
👉 #2 - Custom CLI command
The tool is invoked via gen_api, a custom command that checks if the models exist and execute the core generator. Here is the definition:
# api_generator/commands.py - Truncated content
def gen_api():
# Iterate on models defined in the App Config
for model in API_GENERATOR.values():
# The Model DB existence is checked
try:
models = importlib.import_module("apps.models")
ModelClass = getattr(models, model)
ModelClass.query.all()
except Exception as e:
print(f"Generation API failed: {str(e)}")
return
# All good, we can call the generator
try:
manager.generate_forms_file()
manager.generate_routes_file()
print("APIs have been generated successfully.")
except Exception as e:
print(f"Generation API failed: {str(e)}")
👉 #3 - Core API Generator
This module injects the loads the definition for each model and injects the data into template files with a predefined structure that provides a common structure of an API node:
- GET requests are publically available (no authorization required)
- Mutating requests (Create, Update, Delete) are protected via token_required decorator
Here is the model-dependent service node skeleton used to generate the routes for a single model (truncated content) for GET and CREATE requests.
@api.route('/{endpoint}/', methods=['POST', 'GET', 'DELETE', 'PUT'])
@api.route('/{endpoint}/<int:model_id>/', methods=['GET', 'DELETE', 'PUT'])
class {model_name}Route(Resource):
def get(self, model_id: int = None):
if model_id is None:
all_objects = {model_name}.query.all()
output = [{{'id': obj.id, **{form_name}(obj=obj).data}} for obj in all_objects]
else:
obj = {model_name}.query.get(model_id)
if obj is None:
return {{
'message': 'matching record not found',
'success': False
}}, 404
output = {{'id': obj.id, **{form_name}(obj=obj).data}}
return {{
'data': output,
'success': True
}}, 200
###################################
## Checked for permissions
@token_required
def post(self):
try:
body_of_req = request.form
if not body_of_req:
raise Exception()
except Exception:
if len(request.data) > 0:
body_of_req = json.loads(request.data)
else:
body_of_req = {{}}
form = {form_name}(MultiDict(body_of_req))
if form.validate():
try:
obj = {model_name}(**body_of_req)
{model_name}.query.session.add(obj)
{model_name}.query.session.commit()
except Exception as e:
return {{
'message': str(e),
'success': False
}}, 400
else:
return {{
'message': form.errors,
'success': False
}}, 400
return {{
'message': 'record saved!',
'success': True
}}, 200
The composition code provided by the generator using the Books model as input is shown below:
@api.route('/books/', methods=['POST', 'GET', 'DELETE', 'PUT'])
@api.route('/books/<int:model_id>/', methods=['GET', 'DELETE', 'PUT'])
class BookRoute(Resource):
def get(self, model_id: int = None):
if model_id is None:
all_objects = Book.query.all()
output = [{'id': obj.id, **BookForm(obj=obj).data} for obj in all_objects]
else:
obj = Book.query.get(model_id)
if obj is None:
return {
'message': 'matching record not found',
'success': False
}, 404
output = {'id': obj.id, **BookForm(obj=obj).data}
return {
'data': output,
'success': True
}, 200
@token_required
def post(self):
try:
body_of_req = request.form
if not body_of_req:
raise Exception()
except Exception:
if len(request.data) > 0:
body_of_req = json.loads(request.data)
else:
body_of_req = {}
form = BookForm(MultiDict(body_of_req))
if form.validate():
try:
obj = Book(**body_of_req)
Book.query.session.add(obj)
Book.query.session.commit()
except Exception as e:
return {
'message': str(e),
'success': False
}, 400
else:
return {
'message': form.errors,
'success': False
}, 400
return {
'message': 'record saved!',
'success': True
}, 200
In this API-generated code, we can see the Books model information is injected into the service node template using generic patterns without many specializations.
As mentioned before, the mutating requests are controlled by the token_required that checks the user's existence. Using the decorator the developer controls the access and the basic check provided in this version can be easily extended to more complex checks like user roles.
def token_required(func):
@wraps(func)
def decorated(*args, **kwargs):
if 'Authorization' in request.headers:
token = request.headers['Authorization']
else:
return {
'message': 'Token is missing',
'data': None,
'success': False
}, 403
try:
data = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=["HS256"])
current_user = Users.query.filter_by(id=data['user_id']).first()
if current_user is None:
return {
'message': 'Invalid token',
'data': None,
'success': False
}, 403
Once the user is registered, the access token is provided by the /login/jwt/
route based on registered user credentials (username and password).
This free tool is under heavy development for more patterns and features, listed on the README (product roadmap section). In case anyone has a feature request, feel free to use the GitHub Issues tracker to submit a PR (product request).
- ✅ Dynamic DataTables:
Server-side
pagination,Search
, Export - ✅ Stripe Payments:
One-Time
andSubscriptions
- ✅ Async Tasks:
Celery
and Redis