Alex Merced I am a developer, educator, and founder of devNursery.com.

Masonite: MVC web framework for Python

7 min read 2209

Python Masonite Framework

Most Python frameworks typically fall into one of two categories: minimalist frameworks like Flask and FastAPI or more robust frameworks that have batteries included, like the popular Django framework.

Django, however, has a pretty steep learning curve, primarily because its patterns are very different from almost all other Python frameworks. For example, what other frameworks would call controller actions are known as views in Django.

Django projects are organized into apps, which can sometimes muddle the typical MVC organization but provides benefits in modularity. Out of the box, it can be quite complicated to build a REST API without using a library like the DjangoRestFramework. Django’s folder structure often results in multiple folders and subfolders with the same name, causing confusion among teams.

While many developers still use and love Django, there is a recognized need for a framework that follows more conventional framework patterns, similar to those used by Ruby’s Rails, PHP’s Laravel, or NestJS. The Masonite framework allows us to use Python with a more robust, familiar convention.

In this tutorial, we’ll become familiar with Masonite by building a simple to-do list. To follow along with this tutorial, you’ll need to have Python ≥v3.6 installed. First, let’s review some of Masonite’s features and perks.

Masonite features

  • Inbuilt email support with the MailProvider, SMTP, and MailgunDriver
  • IOC container and auto-resolving dependency injection
  • Service Providers that easily add functionality to the framework
  • Extremely simple static files configuration
  • Masonite ORM: drop-in replacement for Orator similar to Active Record
  • Includes a useful command line tool called the craft command
  • Send WebSocket requests from your server with the Broadcast Provider, pusher, ably, and pubnub drivers
  • Extremely extendable

Masonite terminology

Masonite’s terminology is familiar to that of Rails, Laravel, and most other MVC web frameworks:

  • Controllers: classes that contain several methods called actions
  • Actions: controller method that determines how to respond to particular request
  • Routes: rules that determine which URL or method combinations should be directed to which controller actions

Creating a new Masonite project

First, let’s create a new virtual environment:

python -m venv venv

To activate your virtual environment, run source ./venv/bin/activate. To deactivate, use the command deactivate. Next, install Masonite as follows:

pip install masonite

Create a new project with the command below:

craft new firstproject

Next, navigate to the firstproject directory. Run the following code to install additional dependencies:

We made a custom demo for .
No really. Click here to check it out.

craft install 

In essence, craft is a command that acts as a prefix when working in Masonite, similar to the rails and artisan commands in Rails and Laravel.

Masonite controllers

Now, we’ll create our first controller:

craft controller First

The command above will create the app/http/FirstController.py file, which contains the following code:

"""A FirstController Module."""

from masonite.request import Request
from masonite.view import View
from masonite.controllers import Controller


class FirstController(Controller):
    """FirstController Controller Class."""

    def __init__(self, request: Request):
        """FirstController Initializer

        Arguments:
            request {masonite.request.Request} -- The Masonite Request class.
        """
        self.request = request

    def show(self, view: View):
        pass

In the __init__ function definition, we declare a parameter called request. request uses dependency injection to make the request object, which contains URL params, queries, etc., available to all our routes. Alternately, you could declare parameters on an action by action basis, like the view parameter on the show method.

Rendering templates and views

Let’s say you’re building a full-stack application that will render a template; we’ll use the view parameter that we passed into the show method, rendering the desired template:

def show(self, view: View):
return view.render("helloworld", {"phrase": "Hello World"})

The code above tells Masonite to look in /resources/templates for a template called helloworld.html and render it using the data in the dictionary that is passed as the second argument.

To create the accompanying view, run the command craft view helloworld. Finally, add the code below to the newly created resources/templates/helloworld.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello World</title>
</head>
<body>
    <h1>{{ phrase }}</h1>
</body>
</html>

Creating Masonite routes

To see the code above at work, we just need to connect the controller to a route so that it can render the view. Open up routes/web.py, and let’s add our route as follows:

"""Web Routes."""

from masonite.routes import Get, Post

ROUTES = [
    Get("/", "[email protected]").name("welcome"),
    Get("/first", "[email protected]").name("first")
]

We use the Get function to declare a route for a GET request. The first string denotes the URL of the route, and the second string denotes the action from the controller that should be invoked. In our case, we invoke show from FirstController. The name method allows us to give our route a name, which we’ll use to refer to it in links within our templates.

Now, you can run the server with the command craft serve. We can see our page rendered on localhost:8000/first. That was pretty easy!

Sending JSON data

Sending JSON data is quite straightforward. We’ll return either a dictionary or an array in our action. If we modify our show action in FirstController as follows, when you check the page in the browser, you’ll be greeted by JSON data:

def show(self, view: View):
return {"Hello": "World"}

Working with migrations

To set up our application for migrations, first, we’ll configure the database details in the .env file:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=masonite
DB_USERNAME=root
DB_PASSWORD=root
DB_LOG=True

The DB_CONNECTION field corresponds with the different presets in config/database.py. For now, let’s comment all of these out, causing Masonite to default to SQLite3:

#DB_CONNECTION=mysql
#DB_HOST=127.0.0.1
#DB_PORT=3306
#DB_DATABASE=masonite
#DB_USERNAME=root
#DB_PASSWORD=root
#DB_LOG=True

Now, let’s run a migration to migrate out the default user auth migration that comes out of the box and create our sqlite3 database:

craft migrate

Next, let’s create a migration to create a todos table:

craft migration create_todos_table --create todos

The code above generates a new file in databases/migrations that looks like the code below:

"""CreatePostsTable Migration."""

from masoniteorm.migrations import Migration


class CreatePostsTable(Migration):
    def up(self):
        """
        Run the migrations.
        """
        with self.schema.create("todos") as table:
            table.increments("id")

            table.timestamps()

    def down(self):
        """
        Revert the migrations.
        """
        self.schema.drop("todos")

The up function represents what happens when we run the migration, while the down migration represents what happens when we rollback the migration or drop the table. Next, let’s add some fields for the todo table in the up function:

    def up(self):
        """
        Run the migrations.
        """
        with self.schema.create("todos") as table:
            table.increments("id")

            table.string("detail")
            table.boolean("completed")

            table.timestamps()

In the code block above, we added a string field called detail and a boolean field called completed. Now, let’s run the migration and create the table in our database:

craft migrate

Creating a model

Now, we have a table for our Todos. All we need now is a model that allows us to interact with the data:

craft model Todo

Let’s create the file app/Todo.py with our model class:

"""Todo Model."""

from masoniteorm.models import Model


class Todo(Model):
    """Todo Model."""
    pass

Currently, all of our model methods are attained in the Todo class by inheriting them from Masonite’s Model class.

Usually, the table name is the plural version of the class name, ie. Dog and Dogs. While the table should work automatically in our case, let’s explicitly declare the database table that it should connect to:

"""Todo Model."""

from masoniteorm.models import Model


class Todo(Model):
    """Todo Model."""
    __table__ = 'todos'

Adding to-do items in the Python shell

Run the command below to open up a Python shell with a preloaded Masonite container:

craft tinker

Then, we’ll import our todo model as follows:

from app.Todo import Todo

Now, let’s create a few to-do list items by adding the following code snippet several times:

Todo.create(detail="Hello", completed=False)

After you’ve added a few to-do list items, exit the Python shell with quit(). Now, let’s import the model into our controller and send back all those to-do list items as JSON. Add the code below in your app/http/controllers/FirstController.py file:

"""A FirstController Module."""

from masonite.request import Request
from masonite.view import View
from masonite.controllers import Controller
from app.Todo import Todo


class FirstController(Controller):
    """FirstController Controller Class."""

    def __init__(self, request: Request):
        """FirstController Initializer

        Arguments:
            request {masonite.request.Request} -- The Masonite Request class.
        """
        self.request = request

    def show(self, view: View):
        return Todo.all()

Turn on the server with craft serve and check out the result on localhost:8000/first.

CORS

Before we deploy our application, we need to set up our CORS headers in case we receive external requests to our application.

Let’s import and add the CORSProvider, a built-in provider that helps configure the CORS middleware, into the Providers array. Add the following code block to /config/providers.py:

"""Providers Configuration File."""

from masonite.providers import (
    AppProvider,
    CorsProvider, # ADD THIS
    RequestHelpersProvider,
    AuthenticationProvider,
    BroadcastProvider,
    CacheProvider,
    CsrfProvider,
    HelpersProvider,
    MailProvider,
    QueueProvider,
    RouteProvider,
    SessionProvider,
    StatusCodeProvider,
    UploadProvider,
    ViewProvider,
    WhitenoiseProvider,

)
from masonite.logging.providers import LoggingProvider
from masonite.validation.providers import ValidationProvider
from masoniteorm.providers import ORMProvider

"""Providers List
Providers are a simple way to remove or add functionality for Masonite
The providers in this list are either ran on server start or when a
request is made depending on the provider. Take some time to can
learn more more about Service Providers in our documentation
"""

PROVIDERS = [
    # Framework Providers
    AppProvider,
    CorsProvider, # ADD THIS
    RequestHelpersProvider,
    CsrfProvider,
    AuthenticationProvider,
    SessionProvider,
    RouteProvider,
    StatusCodeProvider,
    WhitenoiseProvider,
    ViewProvider,
    # Optional Framework Providers
    MailProvider,
    UploadProvider,
    QueueProvider,
    CacheProvider,
    BroadcastProvider,
    HelpersProvider,
    ValidationProvider,
    # Third Party Providers
    LoggingProvider,
    ValidationProvider,
    ORMProvider,
    # Application Providers
]

Configure CORS middleware

To configure our CORS middleware, first, let’s declare a CORS variable and assign it a dictionary of our cors headers. Add the code below to your /config/middleware.py file:

"""Middleware Configuration Settings."""

from masonite.middleware import (
    ResponseMiddleware,
    MaintenanceModeMiddleware,
    GuardMiddleware,
)

from app.http.middleware.AuthenticationMiddleware import AuthenticationMiddleware
from app.http.middleware.CsrfMiddleware import CsrfMiddleware
from app.http.middleware.LoadUserMiddleware import LoadUserMiddleware
from app.http.middleware.VerifyEmailMiddleware import VerifyEmailMiddleware

"""HTTP Middleware
HTTP middleware is middleware that will be ran on every request. Middleware
is only ran when a HTTP call is successful (a 200 response). This list
should contain a simple aggregate of middleware classes.
"""

HTTP_MIDDLEWARE = [
    LoadUserMiddleware,
    CsrfMiddleware,
    ResponseMiddleware,
    MaintenanceModeMiddleware,
]

"""Route Middleware
Specify a dictionary of middleware to be used on a per route basis here. The key will
be the alias to use on routes and the value can be any middleware class or a list
of middleware (middleware stacks).
"""

ROUTE_MIDDLEWARE = {
    "auth": AuthenticationMiddleware,
    "verified": VerifyEmailMiddleware,
    "guard": GuardMiddleware,
}


## ADD THIS
CORS = {
    'Access-Control-Allow-Origin': "*",
    "Access-Control-Allow-Methods": "*",
    "Access-Control-Allow-Headers": "*",
    "Access-Control-Max-Age": "3600",
    "Access-Control-Allow-Credentials": "true"
}

Deploying our Masonite app to Heroku

To deploy our Masonite to-do list application to Heroku, let’s install Gunicorn and Psycopg2. If you’re aren’t using Heroku Postgres, be sure to install the right drivers for your deployment platform:

pip install psycopg2
pip install gunicorn

Create a file called Procfile in the project root and run the following command:

web: gunicorn wsgi

Next, create a runtime.txt file:

python-3.9.0

To create list of current dependencies, run pip freeze > requirements.txt:

backpack==0.1
bcrypt==3.1.7
certifi==2021.5.30
cffi==1.14.6
charset-normalizer==2.0.5
cleo==0.8.1
clikit==0.6.2
crashtest==0.3.1
cryptography==3.4.8
exceptionite==1.0.1
Faker==4.18.0
gunicorn==20.1.0
hfilesize==0.1.0
hupper==1.9.1
idna==3.2
inflection==0.3.1
Jinja2==2.11.3
MarkupSafe==2.0.1
masonite==3.0.12
masonite-dot==0.0.5
masonite-logging==1.0.1
masonite-orm==1.0.55
masonite-validation==3.0.14
passlib==1.7.4
pastel==0.2.1
pendulum==2.1.2
psutil==5.6.7
psycopg2==2.9.1
pycparser==2.20
pylev==1.4.0
python-dateutil==2.8.2
python-dotenv==0.10.5
pytzdata==2020.1
requests==2.26.0
requests-file==1.5.1
simplejson==3.17.5
six==1.16.0
tabulate==0.8.9
text-unidecode==1.3
tldextract==2.2.3
urllib3==1.26.6
whitenoise==4.1.4

If you’re missing any of the dependencies above, or you run into issues with Psycopg2, you can easily copy the missing dependency to your requirements.txt file.

Next, we’ll push our project up to GitHub and deploy to a new Heroku project. Go to the Resources tab and provision a Heroku Postgres database. Get the credentials for the database and add the following variables to Heroku config vars:

DB_CONNECTION=postgres
DB_HOST=<get value from herok>
DB_PORT=<get value from herok>
DB_DATABASE=<get value from herok>
DB_USERNAME=<get value from herok>
DB_PASSWORD=<get value from herok>
DB_LOG=True

Finally, copy the key variable from your local .env to your Heroku config vars and set a variable. The code for our final application looks like the image below:

Final Python Masonite Application

Conclusion

In this tutorial, we built a simple to-do list application with Masonite, a web framework for Python that uses the MVC model. Masonite offers many of the best features of modern frameworks like PHP, Ruby, and NestJS, bringing them to Python in a slick package.

The CLI is powerful by helping you generate the components of your application while still providing a simple enough workflow to maximize productivity. Now that you are familiar with the fundamentals of Masonite, you should be able to build your own, complex applications. Happy coding!

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Alex Merced I am a developer, educator, and founder of devNursery.com.

Leave a Reply