Customizable & SaaSy REST APIs










by Juan Gutierrez

A little about myself...

  • Software Engineer
  • First conference presentation
  • Soon to be husband
  • Video game/comic book nut
  • Musician
  • http://juannyg.com



Topics

  • Problem statement
  • Context
    • Examples
    • Alternative solutions
  • Demo
  • Pros & Cons
  • Effectiveness
  • Future features
 

Stack

  • Django
  • Django Rest Framework
    • Routers (SimpleRouter)
    • ViewSets

So what's the problem?

  • SaaS company
  • Public API/web services
  • Client business rules
  • How do we ensure:
    • Code maintainability
    • Clarity of customizations
    • Testability
    • Uniform/Centralized logging
    • Meaningful documentation
 

Keep things consistent!

OrderGroove

http://ordergroove.com

Customizations

  • Tax totals
  • Shipping costs
  • Pricing
  • Coupons
  • Bulk Discounts
  • Extra Data
    • PO#s
    • Customer category
  • "Unstructured data"

Possible solutions...

Client code in core...


class OrderView(ViewSetMixin):    # core methods/etc    def shipping_total(self):        # do core stuff        if self.client == 'client1':            # do client1 stuff        elif self.client == 'client2':            # do client2 stuff
 /order

try/except client imports

class OrderView(ViewSetMixin):    # core methods/etc    def shipping_total(self):        # do core stuff        try:            # import client module            # do client stuff        except:            # nevermind        # more core stuff
/order

Custom URLs

  • urls.py maintenance
  • platform fragmentation
  • Clunky :-(

Wouldn't it be nice if...


  • URLs were consistent
  • Methods/Classes was consistent
  • Filenames were consistent
  • Filesystem paths were consistent

SaaSy Django Rest Framework

No URL management!
/order
class OrderView(ViewSetMixin):
    # core methods/etc    def shipping_total(self):        # core stuff
/client1/order

from core import OrderView as CoreOrderViewclass OrderView(CoreOrderView):    def shipping_total(self):        # custom code        # super available - if necessary

Methods/Class Consistency


  • Core class
class WebService


  • Client class
from core.service import WebService as CoreWebService

class WebService(CoreWebService)





  • Add to REST_FRAMEWORK settings




  • Add to Client/Merchant model    
class Meta:
        saas_url_param = 'merchant_slug'
        saas_lookup_field = 'slug' 
   
'SAAS': {        'MODEL': 'merchant.models.Merchant',
        'MODULE': 'customizations'
    } 

Django REST Framework SaaSy

SaaSRouter

  • Client identifier - saas_url_parameter
  • Key for client vs core routing
/web/service
/client1/web/service
/client2/web/service

SaaS ViewSet

  • Leverages client identifier in URL keyword arg
  • At request, determine class to use

class ViewSetMixin(object):    ...    def as_view(cls, actions=None, **initkwargs):        ...        def get_cls(...):            # determine cls            return cls        def view(request, *args, **kwargs):            ...            cls = get_cls(...)            self = cls(**initkwargs)            return self.dispatch(                request, *args, **kwargs)        return view













project
├── customizations
│   └──client
│       └── app
│           └── subpackage
│               └── module.py
└── app
    └── subpackage 
        └── module.py

File & Path Consistency

  • One entry point for merchant code
  • Package path is identical to core
  • Module name is identical
  • ViewSet object has module and path information
  • These assumptions are the foundation of this solution


class ViewSetMixin(object):    ...    def as_view(cls, actions=None, **initkwargs):        ...        def get_cls(...):            - use value of saas_url_parameter, if available,
              to lookup merchant record via saas_lookup_field
            - try importing class from merchant path            - if found, cls = merchant class            - otherwise, use core class            return cls
def view(request, *args, **kwargs): # initialize ViewSet return view

 

What have we gained?

  • Inheritance!
  • Permissions
  • Auth
  • Serialization
  • Methods
  • Localized




Demo!



drf_saasy_demo
├── customizations
│   └── tiny-client
│       └── product
│           └── views.py
└── product
    └── views.py

Pros

  • Client extensions are easy to identify
  • Core dictates
    • HTTP method enforcement
    • authentication & permissions
  • Client extension only tweaks what it needs to
  • (Should lead to) less duplication of code
  • Separation of core vs client concerns

Cons

  • Django REST Framework dependancy

How has it worked for us?



Highly dependent on the quality of core classes written, 
the way in which they separate concerns from
each other and from methods within the classes.

Still trying to figure out...


  • Client specific @link/@action


Features to come

  • Routing via auth
  • decorators
  • View
  • Pre-filtered client QuerySets?
 

Thanks to

  • My Parents
  • My soon-to-be wife
  • My team @ OrderGroove
  • Jorge Escobar
  • Michael D'Auria
  • Lance Lovette
  • Django & DRF teams
  • and of course YOU!

Customizable and SaaSy REST APIs

By Juan

Customizable and SaaSy REST APIs

A solution to keep your code base clean in a SaaS environment that is predominantly driven by API web services, which has occasion for minor customization of core web services, based on the needs of your clients.

  • 286