DEV Community

Joseph Mancuso for Masonite

Posted on • Updated on

5 reasons why people are choosing Masonite over Django

Introduction

Masonite is an increasingly popular Python web framework that has garnered a lot of attention in the Python community recently.

I am not here to say that Django is crap and that Masonite is the best thing in the entire world but I have talked with a lot of developers over the past year on why they made the switch from Django to Masonite. Below I will go into some detail on the common reasons I have found between them all and why Masonite may make more sense to use for your next project.

One of these developers in particular, Abram Isola, gave some really excellent feedback on why his contract bid (with an extremely large company) ended up winning over all other bids.

Before we start, here are a few links if you want to follow Masonite to explore a bit further:

Ok, let's get started :)

1. Existing Databases

Some business applications require you to use an existing database in order to create a new integration for them.

Managing an existing database with Django is not particularly difficult. You can use something like inspect db with Django in order to generate the proper models and model schema attributes. The issue really arises when you have to manage several databases within the same application, some of which are existing and some of which you need to manage yourself.

You can still do this with Django with something like database routers but it requires a good amount of setup and testing. When you're starting development on an application, you just want to hit the ground running and start the business logic.

Active Record Pattern ORM

Masonite and Django both use the Active Record Style of ORM but the main difference is that Orator has more of a dynamically passive approach to it.

For example, with Django, you specify the columns on the table by using attributes on the class.

In Django this may look something like:

from django.db import models

class Musician(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    instrument = models.CharField(max_length=100)
Enter fullscreen mode Exit fullscreen mode

So now whenever you retrieve, store and exchange data between your model and the database, Django knows what type of data to handle.

Orator is a bit more dynamic. Orator will use the schema the table has in order to map the values of the attributes. In fact, Orator does not need explicit definitions of attributes at all. A comparable model in Masonite would look like this:

from config.database import Model

class Musician(Model):
    pass
Enter fullscreen mode Exit fullscreen mode

Notice we do not specify a firstname, lastname or instrument because Orator knows when it fetches the data from the table, which attributes to set on the model.

2. Class scaffolding

Masonite has what it calls "craft" commands. These are called craft commands because they are a simple command like:

$ craft model Project
Enter fullscreen mode Exit fullscreen mode

that .. "crafts" .. these classes for you so you can stop writing redundant boilerplate over and over again. We all know that the start of a controller starts the same, we all know that the declaration of a function based view starts the same, we all know that a class-based view generally starts the same.

We as developers are constantly writing the same boilerplate over and over again when we get into the rinse-and-repeat cycle of development. Create a file, create some class boilerplate, create the class definition, imports the require classes, etc.

This is why Masonite has over a dozen commands that allow you to create views, controllers, queue jobs, commands, middleware, models, database seeds, etc all from the command line. This saves you and your team a lot of time in development.

Spending time writing out that class definition doesn't only take time from the developer but also breaks the concentration of the developer. You as a developer just wants to generate the class definition and then immediately start working on the business logic that class needs to contain.

Also, the developer no longer needs to remember what base classes or imports are required to be imported in order to get that boilerplate code up and running. Just let the craft command do all the hard work for you so you can keep focusing on your application.

If you want to save a lot of time writing boilerplate code then consider using Masonite for your next application.

3. Folder Structure

This is typically a personal preference and many developers may not share this reason but Masonite lends an extremely solid folder structure.

Masonite has a few different philosophies by default.

One of these is to keep 1 class 1 file. Some developers may think this is normal but many still write multiple classes per file generally. Once you start using the 1 class 1 file system you will never look back.

Another philosophy is to maintain a Separation of Concerns. If you run:

$ craft job SomeJob
Enter fullscreen mode Exit fullscreen mode

for example, it scaffolds that class in the app/jobs directory. Need to craft a middleware? Middleware will be created in the app/http/middleware directory. Need to create a recurring task? Tasks will be created in the app/tasks directory. For on and so forth.

This allows you to not accidentally bundle up all your logic in the same place which makes refactoring later on a huge headache. It's generally good practice to keep all related logic together in their respective modules.

Self Contained Apps

Django, on the other hand, has a good starting structure and also boasts the self-contained apps portion of the framework. This allows you to create self-contained packages that can swap around, be removed, plugged into other applications if done correctly, etc.

The issue with this is that it is fantastic in theory, terrible in practice. In fact, it's my opinion that this is a bit of an anti-pattern, at least the way Django does it.

Anybody who has created a self-contained app knows that it works really well for the first few days and then you start to wrestle it as your app models start to depend on other apps. Finally, you end up putting all your models into a single app and then building your entire application into there. Maybe you use other self-contained apps as packages but they usually end up eventually breaking the "self-contained" promise.

I think this is one of Django's biggest sells and it is a bit of an over promise. While Django sells you on a single application that is broken up into self-contained apps, Masonite is building a single app with a very well structured separation of the concerns file structure to keep you rolling.

4. Middleware

Masonite's middleware system allows for some very easy implementations while also staying very complex and dynamic. Abram Isola chose Masonite over Django because Masonite's middleware has a couple of capabilities that Django simply does not.

Firstly, Masonite has 2 types of middleware. Both types use the exact same class but it just depends on where you put the class after you create it.

The first type of Masonite middleware is what's called HTTP Middleware. This middleware runs of every request like you would normally expect.

HTTP_MIDDLEWARE = [
    AuthenticationMiddleware,
]
Enter fullscreen mode Exit fullscreen mode

The second type of middleware is what's called Route Middleware:

ROUTE_MIDDLEWARE = {
    'auth': AuthenticationMiddleware,
}
Enter fullscreen mode Exit fullscreen mode

This is also the same middleware class you wrote before, just inside a different list a few lines down the config file. Route middleware is a bit more flexible. You can attach it to a specific route:

ROUTES = [
    Get('/dashboard', 'DashboardController@show').middleware('auth')
]
Enter fullscreen mode Exit fullscreen mode

or a group of routes:

ROUTES = [
    RouteGroup([
        Get('/dashboard', 'DashboardController@profile'),
        Get('/dashboard/stats', 'DashboardController@stats'),
        Get('/dashboard/users', 'DashboardController@users'),
    ], middleware=['auth'])
]
Enter fullscreen mode Exit fullscreen mode

If your application needs very precise access controls or simple integrations of middleware, you should consider Masonite for your next project.

Django does have middleware bit it runs on every request and you need to specify inside the middle if it should actually execute any logic based on some input like the request route.

Django Middleware Example

def global_auth_middleware(get_response):
    def middleware(request):
        if not request.user.is_authenticated:
            return redirect('/login/')
        return get_response(request)
    return middleware
Enter fullscreen mode Exit fullscreen mode

Masonite Middleware Example

from masonite.request import Request

class AuthenticationMiddleware:

    def __init__(self, request: Request):
        self.request = request

    def before(self):
        if not self.request.user():
            self.request.redirect_to('login')

    def after(self):
        pass
Enter fullscreen mode Exit fullscreen mode

5. Built in features and drivers

Django and Masonite are both called "batteries included frameworks" but Masonite is boasted as the "Modern and developer-centric Python web framework" because of the types of batteries included features that it contains.

Masonite has features for built-in Amazon S3 uploading, RabbitMQ queue support, Mailgun, and SMTP email sending support, simple database seeding to easily teardown, rebuild and seed your database with data, built in Pusher for websocket support, built in recurring task scheduling and so much more. All of these features that would be "must haves" if it didn't take so much time to integrate them now takes seconds to set up.

  • Does your application have notifications? Easy just use the websocket features to push out notifications to your users.

  • Does your application send email? Queue all of them with RabbitMQ so your application is much more responsive.

  • Does your application handle user uploads? Use the Amazon S3 upload feature to manage them.

  • Does some parts of your application require some tasks to run every hour or every day at midnight? Easy just use the Task scheduling feature to schedule tasks to run every day, every 3 days, or every 5 days at 3 pm easily.

Masonite takes a lot of these features that you may spend several hours researching, implementing and testing and allows you to stay focused on your own business logic and use the integrations we built for you!

Django does have a lot of features out of the box but not remotely as many as Masonite and we are typically adding new features every 2 weeks. You could wake up tomorrow with brand new integration. Update Masonite and you're ready to rock and roll.


Thanks for giving me the time to explain why people are switching from Django to Masonite :)

Be sure to follow Masonite using the links below:

Top comments (16)

Collapse
 
yoongkang profile image
Yoong Kang Lim • Edited

Thanks for introducing me to Masonite, it looks like it has a good API, and I especially like the matcher-style routing.

Would just like to offer a few minor corrections. Firstly, Django's ORM doesn't use the Data Mapper pattern as you described -- it also uses Active Record. In the Active Record pattern, your domain objects contain the logic that handle persistence and querying, which clearly describes Django's models (it even says so here: docs.djangoproject.com/en/2.2/misc...). Perhaps you're confusing Django's ORM with SQLAlchemy, which allows you to use the Data Mapper pattern.

Also, regarding middleware, your comparison between Masonite and Django code examples isn't exactly fair -- they're not doing comparable things, because the auth handling logic in Masonite is clearly done elsewhere, while you put everything in the Django's middleware example. In addition, that is really bad Django code.

In addition, Django's middleware API has also been updated recently -- the one true way to write middleware in Django is now just a function/callable. Here's how Django's middleware that is comparable to your Masonite middleware would look like, assuming it is ordered after the default auth middleware:

def global_auth_middleware(get_response):
    def middleware(request):
        if not request.user.is_authenticated:
            return redirect('/login/')
        return get_response(request)
    return middleware
Enter fullscreen mode Exit fullscreen mode

So, clearly those two code samples you provided comparing Django and Masonite middleware do not do the comparison justice.

Masonite's fine-grained middleware control is a neat idea though.

Collapse
 
josephmancuso profile image
Joseph Mancuso

Fair points. I'll update my post.

As for the Django middleware example, how do you only execute it before or after the request with this style? What would be an example of middleware executing after a request?

Collapse
 
yoongkang profile image
Yoong Kang Lim • Edited

I'm not sure what you mean by "before" a request -- the whole request/response cycle is initiated by a request from the client, anything before that is outside the cycle.

There are hooks you can use for when the request has started (but hasn't finished), or after a http response has been sent to the client (docs.djangoproject.com/en/dev/ref/...), but I must say I've never used them. If I needed to be doing that, I'd be writing WSGI middleware instead, or using something like Celery/Django Channels to do a task asynchronously.

Thread Thread
 
josephmancuso profile image
Joseph Mancuso

yeah been a while since I personally used Django. I think it was beginning of 1.11ish I believe. Just trying to see if there's something comparable to the Masonite middleware example above where its:

--> execute a "before" middleware (cleaning the request, setting some variables on some classes, redirecting for auth, etc)

--> execute the controller method (django view)

--> execute an "after" middleware (setting response headers, setting status code, converting controller method response (i.e dictionary to json converting) etc)

Thanks though!

Thread Thread
 
yoongkang profile image
Yoong Kang Lim

Hmm, maybe I'm misunderstanding you, but:

--> execute a "before" middleware (cleaning the request, setting some variables on some classes, redirecting for auth, etc)

You mean like this?

def global_auth_middleware(get_response):
    def middleware(request):

        if not request.user.is_authenticated:
            return redirect('/login/')
        # before logic here
        request.some_property = Property.objects.get(request['HTTP_X_PROPERTY_ID'])
        return get_response(request)
    return middleware

--> execute an "after" middleware (setting response headers, setting status code, converting controller method response (i.e dictionary to json converting) etc)

Could be something like this.

def global_auth_middleware(get_response):
    def middleware(request):
        if not request.user.is_authenticated:
            return redirect('/login/')
        # 'after' logic here
        response = get_response(request)
        response['X-Custom-Header'] = 'somevalue'
        return response
    return middleware
Collapse
 
spookylukey profile image
Luke Plant

Could you explain why the pattern of not defining database fields on the model itself, but loading them via inspection? I know that Rails does it that way, so some people must prefer it, but I simply cannot imagine wanting that. It seems to have lots of disadvantages.

For example:

  • You're going to need to open a DB tool to know what attributes are available. So without a running app, you can't do the most basic checks on something - or you have to read through and mentally execute the entire migration stack to understand what the current state of the DB schema is, rather than having it all defined in Python.

  • For relationships you need to define it in code (according to this - orator-orm.com/docs/0.9/orm.html#r... ) so the DB fields stuff only goes so far, and know it is split between two places.

  • Computed properties (e.g. a full_name convenience property that combines first_name and surname) are going to be in code, but DB fields in the DB. If you come across my_model_instance.full_name, you won't know whether it is defined in the DB or in Python, so you won't know where to look first.

  • If you want to define additional field info that can't be defined in DB schema (e.g. validation or something like that), there is nowhere for that.

So I'm genuinely curious why this design would be considered an advantage. I always thought that the Rails design was just a quirk of how DHH like to develop.

Collapse
 
josephmancuso profile image
Joseph Mancuso

Good questions. All valid. Laravel does it as well so it usually is the preference at least in both rails and PHP ecosystems. Python seems to have gone the way of how Django does models.

You're going to need to open a DB tool to know what attributes are available. So without a running app, you can't do the most basic checks on something - or you have to read through and mentally execute the entire migration stack to understand what the current state of the DB schema is, rather than having it all defined in Python.

This is correct and this has always been quite a pain point for these types of systems. One of the ways we get around that in Masonite is we have a craft command called craft model:docstring table_name which will generate something like this:

"""Model Definition (generated with love by Masonite)

id: integer default: None
name: string(255) default: None
email: string(255) default: None
password: string(255) default: None
remember_token: string(255) default: None
created_at: datetime(6) default: CURRENT_TIMESTAMP(6)
updated_at: datetime(6) default: CURRENT_TIMESTAMP(6)
customer_id: string(255) default: None
plan_id: string(255) default: None
is_active: integer default: None
verified_at: datetime default: None
"""
Enter fullscreen mode Exit fullscreen mode

you can then slap that on the docstring on your model so you know the details of what is in the table. Just a way to get around that issue.

As for mentally executing the migration stack, I'm not 100% sure what you mean. There is always a migrations table in these types of systems that manage the migrations that have ran and have not run so its usually just a simple command and it will spit back out the state of the database in terms of migrations.

For relationships you need to define it in code (according to this - orator-orm.com/docs/0.9/orm.html#r... ) so the DB fields stuff only goes so far and know it is split between two places.

not really sure what you mean by this one. You don't define relationships on the database level. You can define constraints like foreign key constraints if that is what you mean. But that is a constraint and not necessarily a relationship. If that is what you mean, even if you define a constraint you still need to specify the relationship in code. Getting an entity and its relationships has to do with the query you send to the database.

Django relationships I believe are accessed a little more implicit inside the core codebase but either way, they are defined in code. In order to map a relationship, you need to compile a specific query related to the column of the table either using an implicit or explicit join.

But yeah, either way, you define relationships at the codebase level. You would want to anyway because you can better tweak the relationship. Maybe you only want to limit it to 10 results or order by descending or only show where active is 1.

Computed properties (e.g. a full_name convenience property that combines first_name and surname) are going to be in code, but DB fields in the DB. If you come across my_model_instance.full_name, you won't know whether it is defined in the DB or in Python, so you won't know where to look first.

If you always check the model first you hit 2 birds with 1 stone, if it's not on the model then it's in the table. I wouldn't consider this a bad thing. Whether it's on the model or in the database you still check the model. Plus its easier to check the model than the database usually so yeah. Just check the model. If it's not on the model then its not a computed property and its instead a column on the database.

If you want to define additional field info that can't be defined in DB schema (e.g. validation or something like that), there is nowhere for that.

Why not? Firstly, many of these types of ORM's have hooks that you can tie into like saving or creating. Validation can be done here perfectly fine. Also, validation doesn't have to be at the model level. I can just as easily validate incoming data before I do anything with models. I do this at the controller / business logic to validate all incoming data pretty easily.

So I'm genuinely curious why this design would be considered an advantage. I always thought that the Rails design was just a quirk of how DHH like to develop.

I don't think its a quirk since it's actually a pretty validated design. Laravel itself has been doing it extremely successfully and after using both Django and Laravel (I used Django style first) I much prefer the Laravel style (which is the same Orator and this style above).

I think one of the biggest advantages is a lot of the time on projects, especially in corporations, you are using an existing database. This is the perfect advantage for this type of ORM because with something like Django you need to recreate the entire schema in Python anyway. Yes, there have since been tools to aid in this like database inspection tools to create Python schemas for you but that seems more like an afterthought to me.

I think you should use both systems and experience it for yourself. If you prefer the Django or SQLalchemy way more than more power to you. People have preferences and I fully understand that and that is ok.

Collapse
 
mozillalives profile image
Phil

I agree that this project is interesting and I do like some design considerations.

One design I greatly dislike though is the models. I remember when Django and Rails were first gaining popularity and many of my friends pointed out how cool it was that Rails could infer your model properties from the database schema. As I've gained more experience though I've come to realize that this can be quite annoying and even in some cases dangerous. I very much consider the explicit Django model a plus and would avoid this dynamic ORM.

Interesting work though.

Collapse
 
josephmancuso profile image
Joseph Mancuso

i'm curious, why is it annoying? and why do you think it's dangerous?

Collapse
 
shayneoneill profile image
shayneoneill • Edited

Masonites use of "dynamic" active record is a step backwards. We tried that with Rails. It turned out to be an anti-pattern. (A recurring problem with that confounded ORM. The guy who designed it really did have some ass-backwards views on ORM design. I was baffled at his insistence that constraints didnt belong in the DB.)

I dunno. Some of this I like, but if feels like we're making old mistakes over again.

Collapse
 
josephmancuso profile image
Joseph Mancuso

why do you consider it an anti pattern and how does "non dynamic" active record solve the pattern?

Collapse
 
jasoncrowe profile image
Jason Crowe

Great intro article. It has piqued my interest.

Collapse
 
josephmancuso profile image
Joseph Mancuso

Can't wait for you to join the community :)

Collapse
 
bhupesh profile image
Bhupesh Varshney 👾 • Edited

Will there be a 'django-rest-framework' alternative in Masonite ?
If yes
I am going all over Masonite 😋😋🔥🔥🔥

Collapse
 
josephmancuso profile image
Joseph Mancuso

Yes. There is already the beginnings of ones people are already using called Masonite API and can be found in the docs

Collapse
 
abdurrahmaanj profile image
Abdur-Rahmaan Janhangeer

Thanks, did not know of Masonite!