Generate APIs in Flask without coding - Open-Source Project
Tags: flask, api, generator, tools
Published: 2022-09-21 18:10:00

---
title: Generate APIs in Flask without coding - Open-Source Project
published: true
description: A simple tool for generating secure API services using Flask - Open-source generator that can be extended with ease
tags: flask, api, generator, tools
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p25im0jmb7r9auv7967p.jpg
canonical_url: https://blog.appseed.us/flask-api-generator-open-source-tool/
---
Hello coders!

This article presents an open-source tool able to generate secure APIs using [Flask](https://appseed.us/admin-dashboards/flask/) as backend technology. [Soft UI Dashboard](https://appseed.us/product/soft-ui-dashboard/flask/), 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](https://github.com/app-generator/flask-soft-ui-dashboard) - source code
- 👉 [Soft UI Dashboard Flask](https://appseed.us/admin-dashboards/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

{% embed https://www.youtube.com/watch?v=jrdwFsNLZfw %}

---

## ✨ 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](https://github.com/app-generator/flask-soft-ui-dashboard/blob/cc7c29fe6e173cf68d1af374f6c4dd3a03517286/apps/config.py#L60) section
- Helpers like [token_required](https://github.com/app-generator/flask-soft-ui-dashboard/blob/cc7c29fe6e173cf68d1af374f6c4dd3a03517286/apps/authentication/decorators.py#L10), that check the permissions during runtime
- The [core generator](https://github.com/app-generator/flask-soft-ui-dashboard/tree/master/api_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](https://github.com/app-generator/flask-soft-ui-dashboard/blob/master/apps/models.py) file:

```python
# 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](https://github.com/app-generator/flask-soft-ui-dashboard/blob/cc7c29fe6e173cf68d1af374f6c4dd3a03517286/apps/config.py#L60) section specifies the models to be processed by the generator

```python
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](https://github.com/app-generator/flask-soft-ui-dashboard/blob/master/api_generator/commands.py#L11), a custom command that checks if the models exist and execute the core generator. Here is the definition:

```python
# 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 (**C**reate, **U**pdate, **D**elete) are protected via [token_required](https://github.com/app-generator/flask-soft-ui-dashboard/blob/cc7c29fe6e173cf68d1af374f6c4dd3a03517286/apps/authentication/decorators.py#L10) decorator

Here is the model-dependent [service node skeleton](https://github.com/app-generator/flask-soft-ui-dashboard/blob/master/api_generator/routes/base_route) used to generate the routes for a single model (truncated content) for **GET** and **CREATE** requests.

```python
@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:

```python
@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](https://github.com/app-generator/flask-soft-ui-dashboard/blob/cc7c29fe6e173cf68d1af374f6c4dd3a03517286/apps/authentication/decorators.py#L10) 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.

```python
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.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hxnzm3qb0n0lks7lnaes.png)

---

This free tool is under heavy development for more patterns and features, listed on the [README](https://github.com/app-generator/flask-soft-ui-dashboard/blob/master/README.md) (product roadmap section). In case anyone has a feature request, feel free to use the GitHub [Issues tracker](https://github.com/app-generator/flask-soft-ui-dashboard/issues) 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:

- 👉 [Soft UI Dashboard Flask](https://appseed.us/admin-dashboards/flask/) - tool page (free product)
- ✨ Ask for [support](https://appseed.us/support/) or download [free starters](https://appseed.us/apps/open-source/)