Generate APIs in Flask without coding - Open-Source Project

Generate APIs in Flask without coding - Open-Source Project

A simple tool for generating secure API services using Flask - Open-source generator that can be extended with ease

ยท

5 min read

Hello coders!

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!

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

API Generator for Flask - Open-Source Project - VIDEO Link


โœจ 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).

Flask API Generator - JWT Login.


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 and Subscriptions
  • โœ… Async Tasks: Celery and Redis

Thanks for reading! For more resources and support, please access:

ย