Skip to main content

Adding Features

This guide provides step-by-step instructions for the most common extension patterns in AquaGen API.

Adding a New API Endpoint

Six files to create, one registration. Example: GET /api/user/myFeature?date=09/11/2024.

How the request flows

The route is the entry point — it parses the request, calls the service, passes the result to the formatter, and returns the response.

Step 1 — Route (app/routes/)

Define the namespace, request parser, and resource class. The route is the entry point — it parses the request, calls the service, passes the result to the formatter, and returns the marshalled response.

# app/routes/my_feature.py

from flask_jwt_extended import jwt_required, current_user
from flask_restx import Resource, Namespace, reqparse, marshal

from app.services.my_feature_service import MyFeatureService
from app.formatters.my_feature_formatter import MyFeatureFormatter
from app.models.my_feature_models import MyFeatureResponseModel

myFeatureNamespace = Namespace('My Feature', path='/myFeature')

# Register the model on the namespace for Swagger documentation
myFeatureNamespace.add_model(MyFeatureResponseModel.name, MyFeatureResponseModel)

# Request parser — parameters arrive as headers in AquaGen API
requestParser = myFeatureNamespace.parser()
requestParser.add_argument('date', type=str, required=True, location='headers', help='Date is required (DD/MM/YYYY)')

@myFeatureNamespace.route('')
class MyFeatureRoute(Resource):
@jwt_required()
@myFeatureNamespace.expect(requestParser)
@myFeatureNamespace.response(200, 'Success', model=MyFeatureResponseModel)
@myFeatureNamespace.response(400, 'Missing required header')
@myFeatureNamespace.response(401, 'Unauthorized')
def get(self):
args = requestParser.parse_args() # returns 400 automatically if required arg is missing
date = args['date']

data = MyFeatureService(current_user['industryId'], date).get_data()
formatted = MyFeatureFormatter(data, current_user['unitsMapping']).format()

return marshal({'status': 'success', 'status_code': 200, 'data': formatted}, MyFeatureResponseModel), 200

Step 2 — Service (app/services/)

All business logic lives here. Reads current_user for industry context, calls DatabaseSupporter for data.

# app/services/my_feature_service.py

from app.database.database_supporter import DatabaseSupporter

class MyFeatureService:
def __init__(self, industry_id, date):
self.industry_id = industry_id
self.date = date

def get_data(self):
items = DatabaseSupporter.get_my_feature_data(self.industry_id, self.date)
return items

Step 3 — Formatter (app/formatters/)

Transforms the service output into the final JSON structure returned to the client. Formatters do not call services — they only shape data.

# app/formatters/my_feature_formatter.py

class MyFeatureFormatter:
def __init__(self, data, units_mapping):
self.data = data
self.units_mapping = units_mapping

def format(self):
return [
{
'unitId': item['unitId'],
'unitName': self.units_mapping.get(item['unitId'], {}).get('unitName'),
'value': item.get('value')
}
for item in self.data
]

Step 4 — Model (app/models/)

Define the Swagger response shape. Use Model from flask_restx — this is what @marshal_with and @namespace.response() expect. The route imports this and registers it on the namespace with namespace.model(name, model).

# app/models/my_feature_models.py

from flask_restx import fields, Model

MyFeatureResponseModel = Model('MyFeatureResponse', {
'status': fields.String(default='success'),
'status_code': fields.Integer(default=200),
'message': fields.String(),
'data': fields.Raw() # use fields.List(fields.Nested(...)) for typed arrays
})

fields.Raw is fine when data has a variable shape (different per report type). If the shape is fixed, use fields.Nested with a typed sub-model instead — this gives Swagger accurate schema documentation.

Step 5 — Query (app/database/queires_list.py)

Add a static method to the Queries class. Always decorate with @validate_values() — this runs SQL injection checks on all string parameters before the query is built.

warning

Always decorate query methods with @validate_values(). This runs SQL injection checks on all string parameters before the query is built.

# app/database/queires_list.py

@staticmethod
@validate_values()
def get_my_feature_data(industry_id, date):
return f'SELECT * FROM c WHERE c.industryId = "{industry_id}" AND c.date = "{date}"'

Step 6 — Database Method (app/database/database_supporter.py)

Add a static method to DatabaseSupporter that calls the query above. Never build query strings inline here — always delegate to Queries.

warning

Never build query strings inline in DatabaseSupporter — always delegate to Queries.

# app/database/database_supporter.py

@staticmethod
def get_my_feature_data(industry_id, date):
results = devices_data_container.query_items(
query=Queries.get_my_feature_data(industry_id, date),
enable_cross_partition_query=True
)
dataframe = pd.DataFrame(results)
dataframe = dataframe.replace({np.nan: None})
return dataframe.to_dict(orient="records")

Step 7 — Register the namespace (app/apis/user.py)

from app.routes.my_feature import myFeatureNamespace

api.add_namespace(myFeatureNamespace)

That's it — the endpoint is live at GET /api/user/myFeature.

Naming Conventions

Follow these naming conventions consistently across all new files and classes.
ComponentConventionExample
Route filessnake_case.pysolar_panel.py
Namespace classcamelCaseNamespacesolarPanelNamespace
Service classesPascalCase + ServiceSolarPanelService
Report classesPascalCase + ReportSolarEnergyReport
Formatter classesPascalCase + FormatterSolarDataFormatter
Database methodsget_ / create_ / update_ prefixget_solar_data_by_date
Templatessnake_case_report.htmlsolar_energy_report.html