Login

Django Standard API Response Middleware for DRF for modern frontend easy usage

Author:
Denactive
Posted:
April 20, 2022
Language:
Python
Version:
3.2
Score:
0 (after 0 ratings)

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

  1. LazyPrimaryKeyRelatedField by LLyaudet 2 days, 12 hours ago
  2. CacheInDictManager by LLyaudet 2 days, 18 hours ago
  3. MYSQL Full Text Expression by Bidaya0 3 days, 12 hours ago
  4. Custom model manager chaining (Python 3 re-write) by Spotted1270 1 week, 2 days ago
  5. EnhancedQuerySet by LLyaudet 1 month, 1 week ago

Comments

Please login first before commenting.