Routing and the MVC pattern
Server architecture
Dr Graeme StuartServer architecture
Dr Graeme StuartWe can use export and import statements to break our project across multiple files.
We can explicitly export variables, functions (and classes) from a module by adding the export keyword to the declaration.
| |
Alternatively, we can add an export list to the end of the file like this. As long as the names we include exist, they will be exported.
| |
In another JavaScript module, we can use the import declaration to access exported variables, functions and classes from another module.
| |
In this lecture, we will be carefully considering how to structure our web server.
A request handler is a function that takes a request and returns a response. In these cases, we are exporting them from their own modules.
A simple example ignores the request.
| |
More commonly, return an HTML response.
| |
A handler might also be dynamic, responding to the request more directly
| |
Its common to create a fallback handler for requests which don’t match anything we have
| |
Request handlers MUST return a response. In deno, if your code crashes, as 500 response is automatically returned.
In web development a route is a URL path (e.g. /hello/world) that maps to a specific request handler function on the server.
| |
Routing is the mechanism that matches an incoming request (URL and method) to an existing route and passes the request on to the appropriate route handler function.
Structuring our code from the beginning well will allow our project to expand smoothly.
The overall logical structure involves a router and an MVC pattern.
Our files can be organised in different ways, but this is a good example.
| |
The request must be routed to a controller which will coordinate between models and views.
SQLite is a C-language library that implements a fast and reliable SQL database engine. SQlite databases can be created in memory or in simple files so no database server is required.
We will use the
jsr:@db/sqlitelibrary for integrating SQLite into our projects. Install the library using the following command in a terminal.
| |
To connect to a database, we need a
db.jsfile which exports an instance of the database connection object.
| |
We can use this Database object to execute SQL in any scripts we need and within our models.
| |
All our models can use SQL to interact with our SQLite database via db.exec and db.prepare.
Adding a deno.json file to our project allows us to, amongst other things, define tasks that we can execute using the
deno tasksub-command.
{
"tasks": {
"serve": "deno --watch --allow-all main.js",
"db:init": "deno --allow-all ./tasks/db-init.js"
}
}
deno task serve
deno task db:init
Setting up shortcuts like this will help us to easily rebuild our database from scratch. We can also implement other helper scripts, for example to import some seed data.
The model, view, controller pattern is a tried and tested approach to maintain a separation of concerns between different aspects of our application.
Models are responsible for interfacing with the data persistence mechanism.
| |
Views are responsible for generating the user interface. In this case that means functions that return HTML strings.
| |
Controllers make decisions to generate a response. They mediate between the model and the view.
| |
Controllers are request handlers with a bit more structure. They respond to user interaction (i.e. requests) and decide how to generate a response. They are supported by models and views which handle the details.
Models handle data. They provide functions for controllers to use.
Our model files encapsulate knowledge of the SQL table structure. There should be no SQL code in our controllers or views.
Inside the models folder, we will have modules for each aspect of our data. This might mean one file per database table.
| |
| |
This neutral interface means controllers don’t need to know where or how the data are stored. All they need to know is what functions to call.
Views handle HTML. They also provide functions for controllers to use.
Our user interface will be built on the server as HTML strings. Our view files encapsulate knowledge of the user interface structure. There should be no HTML code in our controllers or models.
Inside the views folder, we will have modules for each aspect of our user interface. This might mean one file per URL.
| |
| |
This neutral interface means controllers don’t need to know how the user interface works. All they need to know is what functions to call.
Convenience functions can be reused by many controllers. e.g. for generating HTML or redirect responses.
| |
| |
These can get more complex as necessary as your application grows.
Controllers are request handlers with more structure. They are the primary decision-making code but they don’t care about how data are stored or how the HTML is structured. Models and views should only be invoked from controllers (or possibly from tasks).
| |
| |
Controllers access data from models and generate responses by either rendering views or redirecting the browser to another route.
We need to distinguish between collections (e.g. /items) and individual resources (e.g. /items/123) with dynamic values.
The URLPattern API allows us to create patterns that match our application routes.
| |
We can test the patterns against URLs.
| |
We can extract the (so-called named groups) from URLs.
| |
This gives us our primary key data and allows us to create a URL for each individual record.
The routing context object allows for a more general purpose controller interface.
| |
Rather than taking a request as an argument, controllers (and eventually our router and middleware) can take an object which has the request as a property.
If you have any questions, now is a good time to ask.
Thanks for listening
Dr Graeme Stuart