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.
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.
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.
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
| Component | Convention | Example |
|---|---|---|
| Route files | snake_case.py | solar_panel.py |
| Namespace class | camelCaseNamespace | solarPanelNamespace |
| Service classes | PascalCase + Service | SolarPanelService |
| Report classes | PascalCase + Report | SolarEnergyReport |
| Formatter classes | PascalCase + Formatter | SolarDataFormatter |
| Database methods | get_ / create_ / update_ prefix | get_solar_data_by_date |
| Templates | snake_case_report.html | solar_energy_report.html |