I'm typescript frontend developer and i was interested in standartizing API server responses. I've tried Django for one of my projects. I've build my API and splited it into Django apps aiming at possible migration to [link >] Microservices [<] later.
The problem I've faced is a difficulty of standartization API responses not only in multiple views, but for all composition of JSON-oriented django-apps which can only be made with middleware.
I have put all the links to everybody could familiarize with Django framework conceptions used here.
Also, I suggest to familiarize with [link >] [origin solution] (https://djangosnippets.org/snippets/10717/) [<].
The main difference is that in all my DRF JSONRenderers I do not need to wrap fields in 'result' or 'results' nested JSON. I do not get messed with result and results. If I expect an array, I just check additional pagination fields.
I did not used a pagination section in my project, still i've left opportunities for that according to [link >] [origin solution] (https://djangosnippets.org/snippets/10717/) [<. Ypu can also find a paginator code fro DRF there.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | import json
import copy
from django.utils.translation import gettext_lazy
from django.core.serializers.json import DjangoJSONEncoder
from rest_framework.response import Response
# origin
# https://djangosnippets.org/snippets/10717/
# https://docs.djangoproject.com/en/4.0/ref/request-response/#httpresponse-object
class ApiWrapperMiddleware:
def __init__(self, get_response):
self.get_response = get_response
# One-time configuration and initialization.
self.pagination_response_keys = ['pag_cnt', 'pag_page_size', 'pag_cur', 'pag_next', 'pag_prev']
self.default_response_keys = [
'status', 'data', 'msg', # basic custom fields
'detail', 'non_field_errors', # DJANGO error indicators
# 'pag_cnt', 'pag_page_size', 'pag_cur', 'pag_next', 'pag_prev', # pagination fields
] + self.pagination_response_keys
# I suggest using message translation map
self.lang_localization_map = {
'en-us': {
"internal": "Internal Server Error",
"unknown": "Unknown Server Error",
"success": "Success",
"pag_err": "Have you just tried to use pagination? \
Make sure you've provided all this fileds: " + ', '.join(self.pagination_response_keys),
},
'ru-ru': {
"internal": "Сервис недоступен",
"unknown": "Сервис недоступен",
"success": "Успех",
"pag_err": "Вы хотели использовать пагинацию? \
Убедитесь, что верно указали все следующие поля: " + ', '.join(self.pagination_response_keys),
}
}
# probably you may like to use LANGUAGE_CODE from settings
# than import it here and there from common config to
# resolve cycle dependency
self.LANGUAGE_CODE = 'ru-ru'
if self.LANGUAGE_CODE == None:
self.LANGUAGE_CODE = 'en-us'
if self.LANGUAGE_CODE in self.lang_localization_map:
self.localization = self.lang_localization_map[self.LANGUAGE_CODE]
else:
print('[ api_wrapper_middleware ]', self.LANGUAGE_CODE,
'encoding is not supported. English message description is used')
self.localization = self.lang_localization_map['en-us']
print('[ api_wrapper_middleware ] is active!')
def __call__(self, request):
# Code to be executed for each request before
# the view (and later middleware) are called.
response = self.get_response(request)
# Code to be executed for each request/response after
# the view is called
# We need to assure if DRF Response type AND is JSON AND data is really JSON-object
# that might be FileResponnse or HTTPResponse from your other django-apps
if isinstance(response, Response):
print('[ api_wrapper_middleware ] [C-T]:',response.get('Content-Type'))
# __ response.get('Content-Type') __ usually looks like for DRF: __ application/json; charset=utf-8 __
if response.get('Content-Type').lower().find('application/json') != -1 and \
isinstance(response.data, dict):
print('[ api_wrapper_middleware ] FOUND JSON!!!')
try:
response_data = self.render_response(response)
response.data = response_data
response.content = json.dumps(response_data, cls=DjangoJSONEncoder)
except Exception as e:
print('[ api_wrapper_middleware ] [ERROR]', e)
pass
else:
print('[ api_wrapper_middleware ] ignored')
return response
def render_response(self, response):
"""
function to fixed the response API following with this format:
__Default Response Structure__
[1] success single
{
"status": int64, // <http-code>,
"msg": "", // <empty on success>
"data": object,
}
[2] success list
{
"status": int64, // <http-code>,
"msg": "", // <empty on success>
"data": object[],
"count": int64,
"page_size": int64,
"cur_page": int64,
"next": int64, // <cur_page + 1 or so>,
"prev": null,
}
[3] failed
{
"status": int64, // <http-code> 4** and 5**,
"msg": string, // <The error message>
"data": {}, // empty object
}
"""
data_copy = copy.deepcopy(response.data)
response_data = {
'data': {},
'msg': "",
'status': response.status_code,
}
# classic django error message propogation mechanism suggest using 'detail' key
# https://www.django-rest-framework.org/api-guide/exceptions/#custom-exception-handling
# 'non_field_errors' key appearce when dealing with forms
# https://docs.djangoproject.com/en/4.0/ref/forms/api/#django.forms.Form.non_field_errors
# updating the response msg
if 'detail' in data_copy:
response_data.update({'msg': data_copy.get('detail')})
del data_copy['detail']
# this may help to display form validation error | probably it is better to do in
# your frontend part. I do so and I've commented corresponding strings
elif 'non_field_errors' in data_copy:
# response_errors = '<br />'.join(data_copy.get('non_field_errors'))
# response_data.update({'msg': response_errors})
response_data.update({'msg': data_copy['non_field_errors']})
del data_copy['non_field_errors']
# store the internal errors messages. Responses with 4** and 5** codes are considered to be errors
# https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#1xx_informational_response
if response.status_code >= 400:
response_errors_msgs = []
response_errors_keys = []
for (key, value) in data_copy.items():
# DRF places its error messages in JSON key-value pair as a value
# of corresponing request key field. So all key-value pairs that
# are not follow __Default Response Structure__ are error messages
# E.g. I performed registration with 3 fields: email (unique), login (unique) and password.
# When I try to register anather user with same parametres, DRF returns the following:
# { "email": "user with this email already exists." }
# and there is no other correct fields
if key not in self.default_response_keys:
errors = ' '.join([str(v) for v in value])
response_errors_msgs.append('%s: %s' % (key, errors))
response_errors_keys.append(key)
if len(response_errors_msgs) > 0:
# if you want to directly display it on form, uncomment the following
# response_errors_msgs = '<br />'.join(response_errors_msgs)
response_errors_msgs = '\n'.join(response_errors_msgs)
response_data.update({'msg': response_errors_msgs})
# deleting the errors in the field keys.
# makes no sence if all the extra fields are considered as errors
# if len(response_errors_keys) > 0:
# list(map(data_copy.pop, response_errors_keys))
if not response_data.get('msg'):
if response.status_code < 500:
response_data.update({'msg': gettext_lazy(self.localization['unknown'])})
else:
response_data.update({'msg': gettext_lazy(self.localization['internal'])})
# 1** codes are information messages. I Consider data to be empty. Not so useful.
elif response.status_code >= 100 and response.status_code < 200:
if not response_data.get('msg'):
response_data.update({'msg': gettext_lazy(self.localization['success'])})
# 2** and 3** codes
else:
if all([x in data_copy for x in self.pagination_response_keys]):
for key in self.pagination_response_keys:
response_data.update({key: data_copy[key]})
del data_copy[key]
elif any([x in data_copy for x in self.pagination_response_keys]):
err_msg = self.localization['pag_err']
print('[ api_wrapper_middleware ]', err_msg)
response_data.update({'msg': '\n'.join([response_data.get('msg') + err_msg])})
response_data.update({'data': data_copy})
return response_data
|
More like this
- Template tag - list punctuation for a list of items by shapiromatron 11 months, 1 week ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 2 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
- Help text hyperlinks by sa2812 1 year, 7 months ago
Comments
Please login first before commenting.