© Christopher Pitt and Joe Mancuso 2020
C. Pitt, J. MancusoThe Definitive Guide to Masonitehttps://doi.org/10.1007/978-1-4842-5602-2_2

2. Routing

Christopher Pitt1  and Joe Mancuso2
(1)
Cape Town, South Africa
(2)
Holbrook, NY, USA
 

In the previous chapter, we set things up to be able to start building a Masonite application. We installed Python, a database, and the craft command-line utility.

In this chapter, we’re going to learn about the process of connecting browser URLs to the application code that handles each request. We’ll learn about the different types of requests Masonite can interpret, and we’ll start to build functionality for our example application.

“How Does Masonite Handle Requests?”

The Internet is built around the idea of request/response cycles. The same few things happen, whenever you open a browser and enter a URL:
  1. 1.

    Your browser connects the address you type in (like www.apress.com) with an IP address. IP addresses come in two forms: IPv4 and IPv6. Both of these are types of addresses that are meant to connect different machines together, but are not easy for humans to deal with.

    Things called Domain Name Servers (or DNS for short) have lookup tables which take in the human-readable domain names and give out the IPv4 or IPv6 address back to the browser.

     
  2. 2.

    The browser makes a request to the server, at the end of the IPv4 or IPv6 address (and usually at port 80 or port 443). After DNS resolution, a request to www.apress.com will result in the browser sending a request to 151.101.172.250:443 (the address might be different when you try it, since servers can change their IP addresses).

     
  3. 3.

    The server then has a chance to interpret the request and respond accordingly. Most of the time, the response will be a textual body (which can contain HTML) and some headers describing the server and response body.

     

It’s the third step where Masonite takes over. Masonite applications listen on port 80 and port 443, unless otherwise configured, and are given the HTTP request to interpret.

HTTP means Hypertext Transfer Protocol, and it describes the format in which requests are made and responses are sent. There’s a huge amount of detail I’m glossing over, because it’s largely unimportant to what we’re learning about Masonite. If you’d like to see the full specification, you can find it at https://tools.ietf.org/html/rfc2616.

Masonite takes in an HTTP request, performs some initial formatting on it, and passes that request to a route handler. In order for us to respond to specific requests, we need to create route handlers and the accompanying controllers.

Creating Controllers and Routes

This code can be found at https://github.com/assertchris/friday-server/tree/chapter-2.

In Masonite, we think of routes as the first point of entry into the application, but before we can create new routes, we have to create new controllers.

The craft command has built-in functionality to help us create new controllers with little effort. Inside our project folder, we can use the following command:
craft controller Home
This will create a file, called HomeController.py , in the app/http/controllers folder. Controllers are the glue between HTTP requests and responses. The one we just made looks like this:
"""A HomeController Module."""
from masonite.request import Request
from masonite.view import View
class HomeController:
    """HomeController Controller Class."""
def __init__ (self, request: Request):
    """HomeController Initializer
    Arguments:
        request {masonite.request.Request}...
    """
    self.request = request
    def show(self, view: View):
        pass

This is from app/http/controllers/HomeController.py.

Controllers are ordinary Python classes. What makes them powerful is that they are created and called using a dependency injection container. We’ll dive deeply into what that means, in Chapter 3.

For now, all you need to know is that the Request and View objects you see, there, will automatically be provided. We don’t need to create new instances of this controller, and feed it with those objects, in order for it to function.

Most of the controller code is documentation. For the sake of brevity, we’re going to omit as much of this kind of documentation as possible. You’ll see it in your files (and it’ll still be in ours), but we won’t be repeating it in code listings.

Now that we’ve made a controller, we can connect it to a route. If you open routes/web.py, you’ll notice it already has a defined route. You’ve probably also noticed the existing controller. Forget about those, for a minute. Let’s add our own route, to respond to GET requests at /home:
from masonite.routes import Get, Post
ROUTES = [
    # ...
    Get().route('/home', 'HomeController@show').name('home'),
]

This is from routes/web.py.

This should be enough, right? Let’s start the server up:
craft serve

Older versions of Masonite require a -r flag to make the server restart every time it sees a file change. If your updates aren't showing in the browser, make sure the server is reloading every time a file changes, by checking in the console. If you're not seeing any activity, there, you may need this flag.

When we open the server in a browser (at http://127.0.0.1:8000/home), we see the screen shown in Figure 2-1.
../images/484280_1_En_2_Chapter/484280_1_En_2_Fig1_HTML.jpg
Figure 2-1

Oops! An error

That can’t be normal, can it? Well, let’s go back to the controller code:
from masonite.request import Request
from masonite.view import View
class HomeController:
    def __init__ (self, request: Request):
        self.request = request
    def show(self, view: View):
        pass

This is from app/http/controllers/HomeController.py.

Our route tells Masonite to use the show method, but the show method just passes. For routes to work, they need to return something. The error message tells us this, albeit in a roundabout way: “Responses cannot be of type: None.”

The fix is surprisingly simple. We just need to return something form the show method. A plain string will do:
def show(self, view: View):
    return 'hello world'
This is from app/http/controllers/HomeController.py.
../images/484280_1_En_2_Chapter/484280_1_En_2_Fig2_HTML.jpg
Figure 2-2

Returning a string from show

Success! This may not seem like much, but it’s the first step toward building a functional application.

Let’s review what has happened so far:
  1. 1.

    We opened a browser to http://127.0.0.1:8000/home. The browser created an HTTP request and sent it to that address.

     
  2. 2.

    The Masonite server, started with craft serve -r and listening on port 80, received the HTTP request.

     
  3. 3.

    The Masonite server looked for a route matching /home, with the GET request method. It found a match and looked at which controller and method to use.

     
  4. 4.

    The Masonite server fetched the prevailing Request and View objects, instantiated the controller, and sent those objects to the controller and show methods.

     
  5. 5.

    We told the controller to return a string, for that type of request, which it did. That string was formatted as an HTTP response and sent back to the browser.

     
  6. 6.

    The browser displayed the HTTP response.

    Every route you create will be connected with a method in a controller, or directly to a response file. You’ll need to follow this process often, so it’s important that you get the hang of it now.

     

This is just an ordinary GET route, but there are many different kinds of routes and variations we can use.

Creating Different Kinds of Routes

We’ve kinda glossed over this, but HTTP requests can have different aspects that separate them. We’ve already seen what GET requests look like – the kind that happen when you type and address into your browser.

Different Methods

There are a few other methods:
  • POST – These kinds of requests usually happen when you submit a form in your browser. They are used to signify that the object(s) being conveyed on should be created on the server.

  • PATCH, PUT – These kinds of requests usually don’t happen in the browser, but they have special meaning and operate similarly to POST requests. They are used to signify that the object(s) being conveyed on should be partially changed or overwritten, respectively.

  • DELETE – These kinds of requests also don’t usually happen in the browser, but they operate similarly to GET requests. They are used to signify that the object(s) being conveyed should be removed from the server.

  • HEAD – These kinds of requests do happen in the browser, but they are more about the metadata of the object(s) being conveyed than they are about the object(s) themselves. HEAD requests are ways to inspect the object(s) in question and whether the browser has permission to operate on them.

Using these request methods, requests to the same path (like /room) can mean different things. A GET request can mean that the browser, or person using it, wants to see information about a specific room.

A POST, PATCH, or PUT request may indicate that the user wants to make or change a room, specifying attributes to create or change it with.

A DELETE request may indicate the user wants the room removed from the system.

Different Parameters

Routes (and requests) can also have different kinds of parameters. The first, and easiest to think about, is the kind of parameter that is part of the URL.

You know when you see blog posts, with URLs like https://assertchris.io/post/2019-02-11-making-a-development-app..? That last part of the URL is a parameter, which could be hard coded or could be dynamic, depending on the application.

We can define these kinds of parameters by changing how the route looks:
from masonite.routes import Get, Post
ROUTES = [
    # ...
    Get().route('/home/@name', 'HomeController@show')
    .name('home'),
]

This is from routes/web.py.

Notice how we’ve added /@name to the route? This means we can use URLs like /home/ chris, and that chris will be mapped to @id. We can access these parameters in the controller:
def __init__ (self, request: Request):
    self.request = request
def show(self, view: View):
    return 'hello ' + self.request.param('name')

This is from app/http/controllers/HomeController.py.

The __init__ method (or the constructor) accepts a Request object, which we can access inside the show method. We can call the param method, to get the named URL parameter, which we defined in the route.

Since we only have the show method, and all __init__ is doing is storing the Request object, we can shorten this code:
from masonite.request import Request
from masonite.view import View
class HomeController:
    def show(self, view: View, request: Request):
        return 'hello ' + request.param('name')

This is from app/http/controllers/HomeController.py.

This works, as before, because controller methods are called after their dependencies have been resolved from the dependency injection container.

If you’re using a dependency in a single method, you should probably just accept that parameter in the same method. If you’re reusing it multiple times, it’s a bit quicker to accept the dependency in the constructor.

Another way to parameterize a request is to allow query string parameters. This is when a URL is requested, but ends in syntax resembling ?name=chris. Let’s make the @name part of the route optional and allow for it to be given as a query string parameter instead:
from masonite.routes import Get, Post
ROUTES = [
    # ...
    Get().route('/home/@name', 'HomeController@show')
    .name('home-with-name'), Get().route('/home', 'HomeController@show')
    .name('home-without-name'),
]

This is from routes/web.py.

The quickest and easiest way to make a parameter optional is to define a second route that doesn’t require it to be provided. Then, we have to modify the controller to work with both:
from masonite.request import Request
from masonite.view import View
class HomeController:
    def show(self, view: View, request: Request):
        return 'hello ' + (
            request.param('name') or request.input('name')
        )

This is from app/http/controllers/HomeController.py.

We can access query string parameters using the input method, on the Request object. Wanna know the best part about this code? If we want to respond to POST, PATCH, or PUT requests, we don’t need to change any of this controller code.

We can modify the /home routes to accept GET and POST requests:
from masonite.routes import Get, Post, Match
ROUTES = [
    # ...
    Match(['GET', 'POST'], '/home/@name',
          'HomeController@show').name('home-with-name'),
    Match(['GET', 'POST'], '/home',
          'HomeController@show').name('home-without-name'),
]

This is from routes/web.py.

We have to allow insecure POST requests to these URLs, in the CSRF middleware:
from masonite.middleware import CsrfMiddleware as Middleware
class CsrfMiddleware(Middleware):
    exempt = [
        '/home',
        '/home/@name',
    ]
    every_request = False
    token_length = 30

This is from app/http/middlware/CsrfMiddleware.py.

We’re going to be learning about middleware in Chapter 8 and CSRF protection in Chapter 4. For now, it’s enough to know that POST requests are usually prevented when they come from outside the application.

Browser requests should continue to work, but now we can also make POST requests to these endpoints. The easiest way to test this is to install an app called Postman. Here are the steps for testing:
  1. 1.

    Go to www.getpostman.com and download and install the app. You’ll need to create a free account when you open the app, unless you’ve used Postman before.

     
  2. 2.

    Change the method dropdown from Get to Post and enter the URL httsp:// 127.0.0.1:8000/home.

     
  3. 3.

    Change the data tab from Params to Body and enter name (key) = chris (value).

     
  4. 4.
    Click Send.
    ../images/484280_1_En_2_Chapter/484280_1_En_2_Fig3_HTML.jpg
    Figure 2-3

    Sending POST requests to the server

     

If either the GET or POST request gives you an error, along the lines of “can only concatenate str (not “bool”) to str,” it’s probably because you’re providing neither the route param nor the query string/post body name.

Route Groups

Sometimes you want to configure multiple routes to be similarly named, or to behave in similar ways. We can simplify the /home routes by grouping them together:
from masonite.routes import Get, Match, RouteGroup
ROUTES = [
    # ...
    RouteGroup(
        [
            Match(['GET', 'POST'], '/@name',
                  'HomeController@show').name('with-name'),
            Match(['GET', 'POST'], '/',
                  'HomeController@show').name('without-name'),
        ],
        prefix='/home',
        name='home-',
    )
]

This is from routes/web.py.

If we use RouteGroup instead of Match or Get, we can define common path and name prefixes. This saves a lot of typing and makes it easier to see routes that have something in common.

There are a few more advanced aspects of RouteGroup, but they are best left for the chapters where they are properly explained. Look out for middleware in Chapter 8 and domains (deploying) in Chapter 13.

Exploring Request and Response

While we’re in the controller, let’s look at the Request and Response classes in a bit more detail. We’ve already used a couple Request methods, but there are more to see.

We’ve seen how to request a single named input, but there’s also a way to get all input for a request:
request.all()

This returns a dictionary of input. For HEAD, GET, and DELETE methods, this probably means query string values. For POST, PATCH, and PUT methods, this probably means request body data.

The latter methods can send their body data as URL-encoded values, or even as JSON data.

I say this “probably means” because the latter methods may also have query string values. While this is allowed in most settings, it’s contrary to the HTTP specification. When you’re designing your application to use the latter methods, you should try to steer clear of mixing query strings and body data.

request.all() is useful, in situations where you’re unsure exactly what data you’re expecting. There are variations of this method, which get a bit more specific:
request.only('name')
request.without('surname')

These methods limit the returned dictionary items and exclude the specified dictionary items, respectively.

If you’re not sure what input you’re expecting, but you want to know of certain keys are present, then there’s another method you can use:
request.has('name')

request.has() returns True or False, depending on whether or not the specified key is defined. You could, for instance, alter the behavior of the route method, based on the presence of certain bits of data. Perhaps you want to update a user’s account details, if you detect data specific to that. Or, perhaps you need to reset their password, if you find relevant data in their form submission. Up to you.

Reading and Writing Cookies

One of the ways we remember a user, and store data relevant to their session, is by setting cookies. These can be set and read in the browser, so it’s important to realize that the Masonite defaults protect against this.

Cookies can be set, using this method:
request.cookie('accepts-cookies', 'yes')
We won’t be able to read this, using JavaScript, unless we also disable HTTP-only and server-side encryption:
request.cookie(
    'accepts-cookies',
    'yes',
    encrypt=False,
    http_only=False,
    expires='5 minutes',
)

This code also demonstrates how to set the expiry of cookies. By default, they will expire the moment the user closes their browser, so any long-lived or persistent data must have this value set.

Cookies can be read in a couple ways. The first is by specifying a key:
request.get_cookie('accepts-cookies', decrypt = False)

If you set Encrypt to False, then you need to set Decrypt to False. Otherwise the Decrypt argument may be omitted. If Masonite tries to decrypt a cookie, but fails, then it will delete that cookie. This is a security precaution against cookie tampering.

If you want to manually delete a cookie, you can do so with this method:
request.delete_cookie('accepts-cookies')

Sending Other Kinds of Responses

So far, we’ve only sent plain strings back to the browser. There are a myriad of other responses we could be sending, beginning with JSON responses:
return response.json({'name': 'chris'})
This kind of response will have the appropriate content type and length headers appended to the response. We can make it even shorter, by returning a dictionary:
return {'name': 'chris'}

It’s this kind of magic that makes me enjoy Masonite so! There’s something similar happening when we return plain strings, but this is the first time we’re digging deep enough to know that is what’s happening.

Now, imagine we wanted to redirect the user, instead of returning some renderable response to the browser. We can use the redirect method for that:
return response.redirect('/home/joe')
This isn’t too flexible on its own. We can, however, use a similarly named Request method, to redirect to a named route:
return request.redirect_to(
    'home-with-name',
    {'name': 'chris'},
)

This is one of the main reasons I recommend you always name your routes. If you later want to change the path to the route, all the code that references a named route will continue to function unmodified. It’s also often quicker using a named route than it is reconstructing or hard coding the URL you need.

Creating Views

The final kind of response I want to talk about is the kind that involves HTML. If we’re interested in building a rich UI, plain strings just aren’t going to cut it. We need a way to construct more complex templates, so we can show dynamic and styled interface elements.

Let’s see what it would look like if we make the /home routes show dynamic HTML. The first step is to create a layout file:
<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        @block content
            <!-- template content will be put here-->
        @endblock
    </body>
</html>

This is from resources/templates/layout.html.

It’s a good idea to build our templates to fit inside a layout, so that global changes can be applied in one place. We’ll see this come into play, shortly. For now, let’s create a home template:
@extends 'layout.html'
@block content
    <h1>hello {{ name }}</h1>
@endblock

This is from resources/templates/home.html.

Notice how little this template has to repeat, since we’re extending the layout.html template. This path is relative to the templates folder. Blocks defined in “outer” templates can be overridden by “inner” templates. This means we can define default content, which “inner” templates can replace with more specific content.

Masonite views use a superset of the Jinja2 template syntax, which can be found at http://jinja.pocoo.org/docs. One important difference is that Masonite templates can use @extends syntax in place of {%extends ...%} syntax.

There are a couple things we need to change, in the controller, to use these templates. Firstly, we’re using dynamic data, in the form of {{ name }}. This data needs to be passed into the view. Secondly, we need to specify which view template to load.

Here’s how that code looks:
def show(self, view: View, request: Request):
    return view.render('home', {
        'name': request.param('name') or request.input('name')
    })

This is from app/http/controllers/HomeController.py.

We pass the name data, to the view, by defining a dictionary of dynamic data.

There’s a lot more to learn about the Jinja2 syntax and how Masonite extends it. We’ll explore it more as we build our example application.

Starting the Example Application

Before we begin, I want to stress that the example application is entirely optional to your learning. Each chapter’s example code can be found on GitHub, so you don’t have to retype anything.

That said, we highly recommend that you at least follow along with the development of the example application. We believe you’ll find it easier to remember what you learn if you see it built into something real. More so if you build something real yourself.

This code can be found at https://github.com/assertchris/friday-server/tree/between-chapters-2-and-3.

I listen to many podcasts, so I’d like Friday (my personal assistant and home automation software) to organize and play podcasts on demand. Friday will begin her life as a glorified podcast app.

Let’s start by creating a page through which to search for new podcasts. We need a new controller and template:
craft controller Podcast
craft view podcasts/search
This new controller is exactly the same as the HomeController we created, except in name. We should rename the show method, so it more accurately reflects what we want to show:
from masonite.view import View
class PodcastController:
    def show_search(self, view: View):
        return view.render('podcasts.search')

This is from app/http/controllers/PodcastController.py.

This new view is just an empty file, but it’s in the right location. Let’s give it some markup, so we can tell whether or not it’s being correctly rendered:
@extends 'layout.html'
@block content
    <h1>Podcast search</h1>
@endblock

This is from resources/templates/podcasts/search.html.

Before this will work, we need to add a route. We can start with a RouteGroup, because we expect to add more routes, with similar names and prefixes.
from masonite.routes import Get, Match, RouteGroup
ROUTES = [
    # ...
    RouteGroup(
        [
            Get().route('/', 'PodcastController@show_search')
            .name('-show-search')
        ],
        prefix='/podcasts',
        name='podcasts',
    ),
]

This is from routes/web.py.

If you’re running the craft serve -r command, you only need to go to /podcasts in the browser to see this new page. It looks a bit ugly, so I think we should start applying some styles. Let’s use a tool called Tailwind (https://tailwindcss.com), since it’s easy to get started with:
npm init -y
npm install tailwindcss --save-dev

This adds two new files and one new folder. You can commit the files to Git, but I recommend adding the folder (which is node_modules) to your .gitignore file. You can always recreate it by running npm install.

Masonite provides an easy way to build Sass (https://sass-lang.com) for our application. We can add the following link to our layout file:
<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <link href="/static/style.css" rel="stylesheet" type="text/css">
    </head>
    <body>
        @block content
            <!-- template content will be put here-->
        @endblock
    </body>
</html>

This is from resources/templates/layout.html.

This /static/style.css file doesn’t exist, but that’s because it is being redirected to storage/compiled/style.css. This file is generated from what we put into storage/static/sass/style.css. We can add new styles, to that file, and see them reflected in our application:
@import "node_modules/tailwindcss/dist/base";
@import "node_modules/tailwindcss/dist/components";
@import "node_modules/tailwindcss/dist/utilities";
h1 {
    @extend .text-xl;
    @extend .font-normal;
    @extend .text-red-500;
}
input {
    @extend .outline-none;
    @extend .focus:shadow-md;
    @extend .px-2;
    @extend .py-1;
    @extend .border-b-2;
    @extend .border-red-500;
    @extend .bg-transparent;
    &[type="button"], &[type="submit"] {
        @extend .bg-red-500;
        @extend .text-white;
    }
}

This is from storage/static/sass/style.scss.

This will only work if we’ve installed the Sass library, using pip install libsass or pip3 install libsass. You may, also, not see changes just by refreshing the page. If you aren’t seeing changes, restart the server and clear your browser cache.

I don’t want to go into too much detail about Tailwind, except to say that it is a utility-based CSS framework. That means styles are applied by giving elements classes (inline), or extracting classes in the way we’ve done with these h1 and input selectors.

Let’s also reposition the content so that it sits in the middle of the page:
<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <link href="/static/style.css" rel="stylesheet" type="text/css">
    </head>
    <body>
        <div class="container mx-auto py-4">
            @block content
                <!-- template content will be put here-->
            @endblock
        </div>
    </body>
</html>

This is from resources/templates/layout.html.

Let’s also add a search form and some dummy results:
@extends 'layout.html'
@block content
    <h1 class="pb-2">Podcast search</h1>
    <form class="pb-2">
        <label for="terms" class="hidden">Terms:</label>
        <input type="search" name="terms" id="terms" />
        <input type="submit" value="search" />
    </form>
    <div class="flex flex-row flex-wrap">
        <div class="w-full md:w-2/5 mr-2 flex flex-row pb-2">
            <div class="min-h-full w-48 bg-red-300"></div>
            <div class="p-4 flex flex-col flex-grow">
                <div class="mb-8 flex flex-col flex-grow">
                    <div class="text-xl mb-2">Title</div>
                    <p class="text-base">Description</p>
                </div>
                <div class="flex flex-grow items-center">
                    <div class="w-10 h-10 bg-red-300"></div>
                    <div class="text-sm ml-4">
                        <p class="leading-none">Author</p>
                        <p class="">date</p>
                    </div>
                </div>
            </div>
        </div>
        <div class="w-full md:w-2/5 mr-2 flex flex-row pb-2">
            <div class="min-h-full w-48 bg-red-300"></div>
            <div class="p-4 flex flex-col flex-grow">
                <div class="mb-8 flex flex-col flex-grow">
                    <div class="text-xl mb-2">Title</div>
                    <p class="text-base">Description</p>
            </div>
            <div class="flex flex-grow items-center">
                <div class="w-10 h-10 bg-red-300"></div>
                <div class="text-sm ml-4">
                    <p class="leading-none">Author</p>
                    <p class="">date</p>
                </div>
            </div>
        </div>
    </div>
    <div class="w-full md:w-2/5 mr-2 flex flex-row pb-2">
        <div class="min-h-full w-48 bg-red-300"></div>
        <div class="p-4 flex flex-col flex-grow">
            <div class="mb-8 flex flex-col flex-grow">
                <div class="text-xl mb-2">Title</div>
                    <p class="text-base">Description</p>
                </div>
                <div class="flex flex-grow items-center">
                    <div class="w-10 h-10 bg-red-300"></div>
                    <div class="text-sm ml-4">
                        <p class="leading-none">Author</p>
                        <p class="">date</p>
                    </div>
                </div>
            </div>
        </div>
    </div>
@endblock
This is from resources/templates/podcasts/search.html.
../images/484280_1_En_2_Chapter/484280_1_En_2_Fig4_HTML.jpg
Figure 2-4

Podcast search form and results

Summary

In this chapter, we learned about controllers, routing, and views. We created multiple entry points into our application, accepted multiple request methods, and responded with simple and complex responses.

We also began work on our personal assistant application, got Sass up and running, and started applying styles to custom markup.

In the next chapter, we’re going to learn about some of the more advanced tools Masonite provides, beginning with the dependency injection container.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.111.9