Tutorial
This guide will walk you through the creation of a simple weblog application, which will help you learn the basics of the Marten web framework. It is designed for beginners who want to get started by creating a Marten web project, so no prior experience with the framework is required.
Requirements
This guide assumes that Crystal and the Marten CLI are properly installed already. You can verify that the Marten CLI is properly installed by running the following command:
marten -v
This should output the version of your Marten installation.
What is Marten?
Marten is a web application framework written in the Crystal programming language. It is designed to make developing web applications easy and fun; and it does so by making some assumptions regarding the common needs that developers may encounter when building web applications.
Creating a project
Creating a project is the first thing to do in order to start working on a Marten web application. This creation process can be done through the use of the marten
command, and it will ensure that the basic structure of a Marten project is properly generated.
This can be achieved from the command line, where you can run the following command to create your first Marten project:
marten new project myblog
The above command will create a myblog
directory inside your current directory. This new folder should have the following content:
myblog/
├── config
│ ├── initializers
│ ├── settings
│ │ ├── base.cr
│ │ ├── development.cr
│ │ ├── production.cr
│ │ └── test.cr
│ └── routes.cr
├── spec
│ └── spec_helper.cr
├── src
│ ├── assets
│ ├── emails
│ ├── handlers
│ ├── migrations
│ ├── models
│ ├── schemas
│ ├── templates
│ ├── cli.cr
│ ├── project.cr
│ └── server.cr
├── .editorconfig
├── .gitignore
├── manage.cr
└── shard.yml
These files and folders are described below:
Path | Description |
---|---|
config/ | Contains the configuration of the project. This includes environment-specific Marten configuration settings, initializers, and web application routes. |
spec/ | Contains the project specs, allowing you to test your application. |
src/ | Contains the source code of the application. By default this folder will include a project.cr file (where all dependencies - including Marten itself - are required), a server.cr file (which starts the Marten web server), a cli.cr file (where migrations and CLI-related abstractions are required), and assets , emails , handlers , migrations , models , schemas , and templates folders. |
.editorconfig | Regular .editorconfig which file defines basic indentation coding styles for Crystal. |
.gitignore | Regular .gitignore file which tells git the files and directories that should be ignored. |
manage.cr | This file defines a CLI that lets you interact with your Marten project in order to perform various actions (e.g. running database migrations, collecting assets, etc). |
shard.yml | The standard shard.yml file, that lists the dependencies that are required to build your application. |
Now that the project structure is created, you can change into the myblog
directory (if you haven't already) in order to install the project dependencies by running the following command:
shards install
Marten projects are organized around the concept of "apps". A Marten app is a set of abstractions (usually defined under a unique folder) that contributes specific behaviours to a project. For example, apps can provide models or handlers. They allow to separate a project into a set of logical and reusable components. Another interesting benefit of apps is that they can be extracted and distributed as external shards. This pattern allows third-party libraries to easily contribute models, migrations, handlers, or templates to other projects. The use of apps is activated by simply adding app classes to the installed_apps
setting.
By default, when creating a new project through the use of the new
command, no explicit app will be created nor installed. This is because each Marten project comes with a default "main" app that corresponds to your standard src
folder. Models, migrations, or other classes defined in this folder are associated with the main app by default, unless they are part of another explicitly defined application.
As projects grow in size and scope, it is generally encouraged to start thinking in terms of apps and how to split models, handlers, or features across multiple apps depending on their intended responsibilities. Please refer to Applications to learn more about applications and how to structure your projects using them.
Running the development server
Now that you have a fully functional web project, you can start a development server by using the following command:
marten serve
This will start a Marten development server. To verify that it's working as expected, you can open a browser and navigate to http://localhost:8000. When doing so, you should be greeted by the Marten "welcome" page:
Your project development server will automatically be available on the internal IP at port 8000. The server port and IP can be changed easily by modifying the config/settings/development.cr
file:
Marten.configure :development do |config|
config.debug = true
config.host = "localhost"
config.port = 3000
end
Once started, the development server will watch your project source files and will automatically recompile them when they are updated; it will also take care of restarting your project server. As such, you don't have to manually restart the server when making changes to your application source files.
Writing a first handler
Let's start by creating the first handler for your project. To do so, create a src/handlers/home_handler.cr
file with the following content:
class HomeHandler < Marten::Handler
def get
respond("Hello World!")
end
end
Handlers are classes that process a web request in order to produce a web response. This response can be rendered HTML content or a redirection for example.
In the above example, the HomeHandler
handler explicitly processes a GET
HTTP request and returns a very simple 200 OK
response containing a short text. But in order to access this handler via a browser, it is necessary to map it to a URL route. To do so, you can edit the config/routes.cr
file as follows:
Marten.routes.draw do
path "/", HomeHandler, name: "home"
if Marten.env.development?
path "#{Marten.settings.assets.url}<path:path>", Marten::Handlers::Defaults::Development::ServeAsset, name: "asset"
path "#{Marten.settings.media_files.url}<path:path>", Marten::Handlers::Defaults::Development::ServeMediaFile, name: "media_file"
end
end
The config/routes.cr
file was automatically created earlier when you initialized the project structure. By using the #path
method you wired the HomeHandler
into the routes configuration.
The #path
method accepts three arguments:
- the first argument is the route pattern, which is a string like
/foo/bar
. When Marten needs to resolve a route, it starts at the beginning of the routes array and compares each of the configured routes until it finds a matching one - the second argument is the handler class associated with the specified route. When a request URL is matched to a specific route, Marten executes the handler that is associated with it
- the last argument is the route name. This is an identifier that can later be used in your codebase to generate the full URL for a specific route, and optionally inject parameters in it
Now if you go to http://localhost:8000, you will get the Hello World!
response that is generated by the handler you just wrote.
Multiple routes can map to the same handler class if necessary.
Please refer to Routing to learn more about Marten's routing mechanism.
Creating the Article model
Models are classes that define what data can be persisted and manipulated by a Marten application. They explicitly specify fields and rules that map to database tables and columns. Model records can be queried and interacted with through a mechanism called Query sets.
Let's define an Article
model, which is the linchpin of any weblog application. To do set, let's create a src/models/article.cr
file with the following content:
class Article < Marten::Model
field :id, :big_int, primary_key: true, auto: true
field :title, :string, max_size: 255
field :content, :text
end
As you can see, Marten models are defined as subclasses of the Marten::Model
base class, and they explicitly define "fields" through the use of a field
macro.
In its current state, our Article
model contains the following three fields:
id
is a big integer that will hold the unique identifier of an article (primary key of the underlying table record)title
is a string column (VARCHAR(255)
) that will hold the title of an articlecontent
is a text column (TEXT
) that will hold the textual content of an article
Generating and running migrations
Our Article
model above is defined but is not "applied" at the database level yet. In order to create the corresponding table and columns, we will need to generate a migration for it.
Marten provides a migrations mechanism that is designed to be automatic: this means that migrations will be automatically derived from your model definitions. This allows to ensure that the definition of your model and its fields (and underlying columns) is done in one place only, which helps keep your project DRY.
In order to generate the migration file for the model we created previously, all we need to do is to run the following Marten command:
marten genmigrations
This will output something along those lines:
Generating migrations for app 'main':
› Creating [src/migrations/202208072015231_create_main_article_table.cr]... DONE
○ Create main_article table
The genmigrations
command is a way to tell Marten to introspect your project in order to identify whether you added, removed, or modified models. The changes identified by this command are persisted in migration files (living under migrations/
folders in each Marten application), that you can run later on in order to apply them at the database level.
Now that we have generated a migration file for our Article
model, we can apply it at the database level by running the following command:
marten migrate
Which will output the following content:
Running migrations:
› Applying main_202208072015231_create_main_article_table... DONE
The migrate
command will identify all the migration files that weren't applied to your database yet, and will run them one by one. By doing so, Marten will ensure that the changes you made to your model definitions are applied at the database level, in the corresponding tables.
Please refer to Migrations to learn more about migrations.
For new projects, Marten uses a SQLite database by default. In our case, if we look at the config/settings/base.cr
file, we can see that the current database configuration looks something like this:
config.database do |db|
db.backend = :sqlite
db.name = Path["myblog.db"].expand
end
An SQLite database is a good choice in order to try Marten and experiment with it (since SQLite is already pre-installed on most systems). That being said, if you need to use another database backend (for example, PostgreSQL, MariaDB, or MySQL), feel free to have a look at the databases configuration reference.
Interacting with model records
Now that our Article
model table has been created at the database level, let's try to make use of the Marten ORM to create and query article records.
To do so, we can launch an instance of the Crystal playground as follows:
marten play
You should be able to navigate to http://localhost:8080 and see a Crystal editor containing the following snippet:
require "./src/project"
# Setup the project.
Marten.setup
# Write your code here.
These lines basically require your project dependencies and ensure that Marten is properly set up. You should keep those in the editor when playing with the following examples. Each of the following snippets is assumed to be copied/pasted below the previous one. The output of these examples is highlighted next to the # =>
comment line.
Let's start by initializing a new Article
object:
article = Article.new(title: "My article", content: "This is my article.")
# => #<Article:0x102c4dbe0 id: nil, title: "My article", content: "This is my article.">
As you can see, by using #new
, we are initializing a new Article
object by specifying its field values (title
and content
). It should be noted that so far, the object is only initialized and is not saved to the database yet (this is why id
is set to nil
in the above snippet). In order to persist the new object at the database level, you can make use of the #save
method:
article.save
# => true
The output of the #save
method is a boolean indicating the result of the object validation: in our case, true
means that the Article
object was successfully validated and that the corresponding record was created at the database level.
Now if we inspect the article
object again, we should observe that an id
has been set for the record at hand:
article
# => #<Article:0x104ee1c30 id: 1, title: "My article", content: "This is my article.">
If we want to fetch this record from the database again, we can use the #get
method and specify the identifier value of the record we want to retrieve. For example:
article = Article.get(id: 1)
# => #<Article:0x104c699b0 id: 1, title: "My article", content: "This is my article.">
Now we can try to retrieve all the Article
records that we currently have in the database. This is done by calling the #all
method on the Article
model:
Article.all
# => <Article::QuerySet [#<Article:0x1039296e0 id: 1, title: "My article", content: "This is my article.">]>
This method returns an Article::QuerySet
object, which is commonly referred to as a "query set". A query set is a representation of records collections from the database that can be filtered, and iterated over. The Article::QuerySet
class, automatically generated for the Article
model, is a subclass of Marten::DB::Query::Set
.
Please refer to Queries to learn more about Marten's querying capabilities.
Showing a list of articles
Let's revisit our initial implementation of the HomeHandler
handler we defined earlier.
Since we are building a weblog application, it would make sense to display a list of all our Article
objects on the index page. To do so, let's update the existing src/handlers/home_handler.cr
file with the following content:
class HomeHandler < Marten::Handler
def get
render("home.html", context: { articles: Article.all })
end
end
The #render
method that is used above allows to return an HTTP response whose content is generated by rendering a specific template. The template can be rendered by specifying a context hash or a named tuple. In our case the template context contains an articles
key that maps to a query set of all the Article
records.
Now if you start the Marten development server again and then try to access the home page (http://localhost:8000), you should get an error stating that the home.html
template does not exist. This is normal: we need to create it.
Templates provide a convenient way for defining the presentation logic of a web application. They allow to write HTML content that is rendered dynamically by using variables that you specify in a "template context". This rendering process can involve model records or any other variables you define.
Let's define the expected template for our home handler by creating a src/templates/home.html
file with the following content:
{% extend "base.html" %}
{% block content %}
<h1>My blog</h1>
<h2>Articles:</h2>
<ul>
{% for article in articles %}
<li>{{ article.title }}</li>
{% endfor %}
</ul>
{% endblock %}
As you can see, Marten's templating system relies on variables that are surrounded by {{
and }}
. Each variable can involve lookups in order to access specific object attributes. In the above example {{ article.title }}
means that the title
attribute of the article
variable should be outputted.
Method-calling is done by using statements (also called "template tags") delimited by {%
and %}
. Such statements can involve for loops, if conditions, etc. In the above example we are using a for loop to iterate over the Article
records in the articles
query set that is "passed" to the template context in our HomeHandler
handler.
Please refer to Templates to learn more about Marten's templating system.
What about the extend
and block
tags in the previous snippet? These tags allow to "extend" a "base" template that usually contains the layout of an application (base.html
in the above snippet) and to explicitly define the contents of the "blocks" that are expected by this base template. New marten projects are created with a simple base.html
template that defines a very basic HTML document, whose body is filled with the content of a content
block. This is why templates in this tutorial extend a base.html
and override the content of the content
block.
You can learn more about these capabilities in Template inheritance.
If you go back to the home page (http://localhost:8000), you should be able to see a list of article titles corresponding to all the Article
records you created previously.
We have now pieced together the main components of the Marten web framework (Models, Handlers, Templates). When accessing the home page of our application, the following steps are taken care of by the framework:
- the browser issues a GET request to
http://localhost:8000
- the Marten web application that is currently running receives the request
- the Marten routing system maps the path of the incoming request to the
HomeHandler
handler - the handler is initialized and executed, which involves fetching all the
Article
records - the handler renders the
home.html
template and returns a200 OK
response containing the rendered content - the Marten server sends the response with the HTML content back to the browser
Showing a single article
We presently have a handler that lists all the existing Article
records, but it would be nice to be able to actually see the content of each article individually.
To do so, let's create a new src/handlers/article_detail_handler.cr
handler file with the following content:
class ArticleDetailHandler < Marten::Handler
def get
render("article_detail.html", context: { article: Article.get!(id: params["pk"]) })
rescue Marten::DB::Errors::RecordNotFound
raise Marten::HTTP::Errors::NotFound.new("Article not found")
end
end
Like in the previous example, we will be relying on the #render
method to render a template and return the corresponding HTTP response. But this time we will be retrieving a specific record whose parameter will be specified in a pk
route parameter.
You will note that we are making use of the #get!
method to retrieve the model record in the above example. This method behaves similarly to the #get
method we saw earlier, but it will raise a "record not found" exception if no record can be found for the specified parameters. In that case we can "rescue" this error in order to raise an "Not Found" HTTP exception that will result in a 404 response to be returned.
Let's now map this handler to a new route by adding the following line to the config/routes.cr
file:
Marten.routes.draw do
path "/", HomeHandler, name: "home"
path "/article/<pk:int>", ArticleDetailHandler, name: "article_detail"
if Marten.env.development?
path "#{Marten.settings.assets.url}<path:path>", Marten::Handlers::Defaults::Development::ServeAsset, name: "asset"
path "#{Marten.settings.media_files.url}<path:path>", Marten::Handlers::Defaults::Development::ServeMediaFile, name: "media_file"
end
end
As you can see above, the new route we mapped to the ArticleDetailHandler
handler requires a pk
- primary key - integer (int
) parameter. Route parameters are defined using angle brackets, and the name of the parameter and its type are separated by a :
character (<name:type>
format).
Obviously, we also need to define the article_detail.html
template. To do so, let's create a src/templates/article_detail.html
with the following content:
{% extend "base.html" %}
{% block content %}
<h1>{{ article.title }}</h1>
<p>{{ article.content }}</p>
{% endblock %}
Now if you try to access http://localhost:8000/article/1, you will be able to see the content of the Article
record with ID 1.
There is something missing though: the home page does not link to the "detail" page of each article. To remediate this, we can modify the src/templates/home.html
template file as follows:
{% extend "base.html" %}
{% block content %}
<h1>My blog</h1>
<h2>Articles:</h2>
<ul>
{% for article in articles %}
<li>
{{ article.title }}
‐ <a href="{% url 'article_detail' pk: article.id %}">View</a>
</li>
{% endfor %}
</ul>
{% endblock %}
The url
tag used in the above snippet allows to perform a reverse URL resolution. This allows to generate the final URL associated with a specific route name (the article_detail
route name we defined earlier in this case). This reverse resolution can involve parameters if the considered route require ones.
Please refer to Routing to learn more about Marten's routing system.
Creating a new article
So far we only implemented support for "read" operations: we made it possible to list all the available articles in the home page, and we added the ability to see the content of a specific article in the "detail" page. The next step will be to make it possible to create new articles in order to populate our weblog.
To do so, let's start by creating a new src/schemas/article_schema.cr
schema file with the following content:
class ArticleSchema < Marten::Schema
field :title, :string, max_size: 255
field :content, :string
end
We just defined a "schema". Schemas are classes that define how input data should be serialized / deserialized, and validated. Schemas are usually used when processing web requests containing form data or pre-defined payloads. Like models, they contain a set of pre-defined fields that indicate what parameters are expected, what are their types, and how they should be validated.
Let's see how we can use this schema in a handler. In this light, let's create a new src/handlers/article_create_handler.cr
file:
class ArticleCreateHandler < Marten::Handler
@schema : ArticleSchema?
def get
render("article_create.html", context: { schema: schema })
end
def post
if schema.valid?
article = Article.new(schema.validated_data)
article.save!
redirect(reverse("home"))
else
render("article_create.html", context: { schema: schema }, status: 422)
end
end
private def schema
@schema ||= ArticleSchema.new(request.data)
end
end
This handler is able to handle both GET and POST requests:
- when the incoming request is a GET, it will simply render the
article_create.html
template, and initialize the schema (instance ofArticleSchema
) with any data currently present in the request object (which is returned by the#request
method). This schema object is made available to the template context - when the incoming request is a POST, it will initialize the schema and try to see if it is valid considering the incoming data. If it's valid, then the new
Article
record will be created using the schema's validated data, and the user will be redirect to the home page. Otherwise, thearticle_create.html
template will be rendered again with the invalid schema in the associated context
In the above snippet, we make use of #redirect
in order to indicate that we want to return a 302 Found HTTP response, and we generate the redirection URL by performing a reverse resolution of the home
route we introduced earlier by using the #reverse
method (which is similar to the url
template tag we encountered in the previous section).
We can now create the article_create.html
template file with the following content:
{% extend "base.html" %}
{% block content %}
<h1>Create a new article</h1>
<form method="post" action="" novalidate>
<input type="hidden" name="csrftoken" value="{% csrf_token %}" />
<div><label>Title</label></div>
<input type="text" name="{{ schema.title.id }}" value="{{ schema.title.value }}"/>
{% for error in schema.title.errors %}<p class="input-error"><small>{{ error.message }}</small></p>{% endfor %}
<div><label>Content</label></div>
<textarea name="{{ schema.content.id }}" value="{{ schema.content.value }}">{{ schema.content.value }}</textarea>
{% for error in schema.content.errors %}<p class="input-error"><small>{{ error.message }}</small></p>{% endfor %}
<div><button>Submit</button></div>
</form>
{% endblock %}
As you can see, the above snippet defines a form that includes two fields: one for the title
schema field and the other one for the content
schema field. Each schema field can be errored depending on the result of a validation, and this is why specific field errors are (optionally) displayed as well.
The csrftoken
input in the above example is mandatory because every unsafe request (eg. POST) is automatically protected by a CSRF (Cross-Site Request Forgeries) check. Please refer to Cross-Site Request Forgery protection to learn more about this.
Finally, we need to map the ArticleCreateHandler
handler to a proper route. We can do this by editing the config/routes.cr
file as follows:
Marten.routes.draw do
path "/", HomeHandler, name: "home"
path "/article/create", ArticleCreateHandler, name: "article_create"
path "/article/<pk:int>", ArticleDetailHandler, name: "article_detail"
if Marten.env.development?
path "#{Marten.settings.assets.url}<path:path>", Marten::Handlers::Defaults::Development::ServeAsset, name: "asset"
path "#{Marten.settings.media_files.url}<path:path>", Marten::Handlers::Defaults::Development::ServeMediaFile, name: "media_file"
end
end
Now if you open your browser at http://localhost:8000/article/create, you should be able to see a very rough form allowing to create a new Article
record and to redirect to it.
Obviously, we still need to a link somewhere in our application to be able to easily access the article creation form. In this light, we can modify the home.html
template file as follows:
{% extend "base.html" %}
{% block content %}
<h1>My blog</h1>
<a href="{% url 'article_create' %}">Create new article</a>
<h2>Articles:</h2>
<ul>
{% for article in articles %}
<li>
{{ article.title }}
‐ <a href="{% url 'article_detail' pk: article.id %}">View</a>
</li>
{% endfor %}
</ul>
{% endblock %}
Please refer to Schemas to learn more about schemas.
Updating an article
Now that we are able to create new Article
records, let's add the ability to update existing records. To do so, we will implement a handler that works similarly to the ArticleCreateHandler
handler we defined earlier: it should be able to process GET requests in order to display an "update" form, and it should validate the incoming data (and update the right record) when POST requests are submitted through an HTML form.
In this light, let's define the src/handlers/article_update_handler.cr
file with the following content:
class ArticleUpdateHandler < Marten::Handler
@article : Article?
@schema : ArticleSchema?
def get
render("article_update.html", context: { article: article, schema: schema })
end
def post
if schema.valid?
article.update!(schema.validated_data)
redirect(reverse("home"))
else
render("article_update.html", context: { article: article, schema: schema }, status: 422)
end
end
private def article
@article ||= Article.get!(id: params["pk"])
rescue Marten::DB::Errors::RecordNotFound
raise Marten::HTTP::Errors::NotFound.new("Article not found")
end
private def initial_schema_data
Marten::Schema::DataHash{ "title" => article.title, "content" => article.content }
end
private def schema
@schema ||= ArticleSchema.new(request.data, initial: initial_schema_data)
end
end
Here the #get
and #post
method implementations look similar to what was introduced for the ArticleCreateHandler
handler. The main difference is that a specific Article
record needs to be retrieved. Moreover the ArticleSchema
schema is initialized with some "initial data" (Marten::Schema::DataHash
hash-like object) that corresponds to the current title
and content
field values of the considered record. When a valid schema is processed, instead of creating a new record, we simply update the considered one through the use of the #update!
method.
We can now create the article_update.html
template file with the following content:
{% extend "base.html" %}
{% block content %}
<h1>Update article "{{ article.title }}"</h1>
<form method="post" action="" novalidate>
<input type="hidden" name="csrftoken" value="{% csrf_token %}" />
<div><label>Title</label></div>
<input type="text" name="{{ schema.title.id }}" value="{{ schema.title.value }}"/>
{% for error in schema.title.errors %}<p class="input-error"><small>{{ error.message }}</small></p>{% endfor %}
<div><label>Content</label></div>
<textarea name="{{ schema.content.id }}" value="{{ schema.content.value }}">{{ schema.content.value }}</textarea>
{% for error in schema.content.errors %}<p class="input-error"><small>{{ error.message }}</small></p>{% endfor %}
<div><button>Submit</button></div>
</form>
{% endblock %}
As you can see, this looks very similar to what we did previously with the article_create.html
template file.
Let's now map the ArticleUpdateHandler
handler to a proper route. We can do this by editing the config/routes.cr
file as follows:
Marten.routes.draw do
path "/", HomeHandler, name: "home"
path "/article/create", ArticleCreateHandler, name: "article_create"
path "/article/<pk:int>", ArticleDetailHandler, name: "article_detail"
path "/article/<pk:int>/update", ArticleUpdateHandler, name: "article_update"
if Marten.env.development?
path "#{Marten.settings.assets.url}<path:path>", Marten::Handlers::Defaults::Development::ServeAsset, name: "asset"
path "#{Marten.settings.media_files.url}<path:path>", Marten::Handlers::Defaults::Development::ServeMediaFile, name: "media_file"
end
end
Now if you open your browser at http://localhost:8000/article/1/update, you should be able to see a very rough form allowing to update the Article
record with ID 1.
We can also add a link somewhere in the home page of the application to be able to easily access the update form for existing articles. In this light, we can modify the home.html
template file as follows:
{% extend "base.html" %}
{% block content %}
<h1>My blog</h1>
<a href="{% url 'article_create' %}">Create new article</a>
<h2>Articles:</h2>
<ul>
{% for article in articles %}
<li>
{{ article.title }}
‐ <a href="{% url 'article_detail' pk: article.id %}">View</a>
‐ <a href="{% url 'article_update' pk: article.id %}">Update</a>
</li>
{% endfor %}
</ul>
{% endblock %}
Deleting an article
Finally, the last missing feature we could add is the ability to delete an article. To do so, let's introduce a handler that works as follows: when processing a GET request the handler will ask the user to confirm that they want to indeed delete the considered Article
record, and when processing a POST request the handler will actually delete the record and then redirect to the home page of the application.
In this light, let's define the src/handlers/article_delete_handler.cr
file:
class ArticleDeleteHandler < Marten::Handler
@article : Article?
def get
render("article_delete.html", context: { article: article })
end
def post
article.delete
redirect(reverse("home"))
end
private def article
@article ||= Article.get!(id: params["pk"])
rescue Marten::DB::Errors::RecordNotFound
raise Marten::HTTP::Errors::NotFound.new("Article not found")
end
end
In the above snippet, the #get
method simply fetches the Article
record by using the pk
parameter value and renders the article_delete.html
template (with the article in the associated context). The #post
method fetches the Article
record as well and deletes it before redirecting the user to the home page.
Let's now create the article_delete.html
template file with the following content:
{% extend "base.html" %}
{% block content %}
<h1>Delete article "{{ article.title }}"</h1>
<p>Are you sure?</p>
<form method="post" action="">
<input type="hidden" name="csrftoken" value="{% csrf_token %}" />
<button>Yes, delete</button>
</form>
{% endblock %}
This template simply asks the user for confirmation and displays a confirmation button embedded in a form to issue the POST request that will actually delete the record.
Let's now map the ArticleDeleteHandler
handler to a proper route. We can do this by editing the config/routes.cr
file as follows:
Marten.routes.draw do
path "/", HomeHandler, name: "home"
path "/article/create", ArticleCreateHandler, name: "article_create"
path "/article/<pk:int>", ArticleDetailHandler, name: "article_detail"
path "/article/<pk:int>/update", ArticleUpdateHandler, name: "article_update"
path "/article/<pk:int>/delete", ArticleDeleteHandler, name: "article_delete"
if Marten.env.development?
path "#{Marten.settings.assets.url}<path:path>", Marten::Handlers::Defaults::Development::ServeAsset, name: "asset"
path "#{Marten.settings.media_files.url}<path:path>", Marten::Handlers::Defaults::Development::ServeMediaFile, name: "media_file"
end
end
Now if you open your browser at http://localhost:8000/article/1/delete, you should be able to see a the confirmation page allowing to delete the Article
record with ID 1.
We can also add a link somewhere in the home page of the application to be able to easily access the delete confirmation page for existing articles. In this light, we can modify the home.html
template file as follows:
{% extend "base.html" %}
{% block content %}
<h1>My blog</h1>
<a href="{% url 'article_create' %}">Create new article</a>
<h2>Articles:</h2>
<ul>
{% for article in articles %}
<li>
{{ article.title }}
‐ <a href="{% url 'article_detail' pk: article.id %}">View</a>
‐ <a href="{% url 'article_update' pk: article.id %}">Update</a>
‐ <a href="{% url 'article_delete' pk: article.id %}">Delete</a>
</li>
{% endfor %}
</ul>
{% endblock %}
Refactoring: using template partials
The templates used for creating and updating an article look the same: they both make use of the same schema in order to create or update articles. It would be interesting to be able to reuse this form for both templates. This is where template "partials" came in handy: these are template snippets that can be easily "included" into other templates to avoid duplications of code.
Let's create a src/templates/partials/article_form.html
partial with the following content:
<form method="post" action="" novalidate>
<input type="hidden" name="csrftoken" value="{% csrf_token %}" />
<div><label>Title</label></div>
<input type="text" name="{{ schema.title.id }}" value="{{ schema.title.value }}"/>
{% for error in schema.title.errors %}<p class="input-error"><small>{{ error.message }}</small></p>{% endfor %}
<div><label>Content</label></div>
<textarea name="{{ schema.content.id }}" value="{{ schema.content.value }}">{{ schema.content.value }}</textarea>
{% for error in schema.content.errors %}<p class="input-error"><small>{{ error.message }}</small></p>{% endfor %}
<div><button>Submit</button></div>
</form>
This partial template contains the exact same form that we used in the creation and update templates.
Let's now make use of this partial in the src/templates/article_create.html
and src/templates/article_update.html
templates by leveraging the include
template tag:
{% extend "base.html" %}
{% block content %}
<h1>Create a new article</h1>
{% include "partials/article_form.html" %}
{% endblock %}
{% extend "base.html" %}
{% block content %}
<h1>Update article "{{ article.title }}"</h1>
{% include "partials/article_form.html" %}
{% endblock %}
As you can see, the creation and update templates are now much more simple.
The include
template tag provides additional options like the ability to assign variables that are specific to the included template. Please refer to the include
template tag reference to learn more about this mechanism.
Refactoring: using generic handlers
The handlers we implemented previously map to common web development use cases: retrieving data from the database - from a specific URL paramater - and displaying it, listing multiple objects, creating or updating records, etc. These use cases are so frequently encountered that Marten provides a set of "generic handlers" that allow to easily implement them. These generic handlers take care of these common patterns so that developers don't end up reimplementing the wheel.
We could definitely leverage these generic handlers as part of our weblog application.
In this light, let's start with the HomeHandler
class we implemented earlier: this handler essentially retrieves all the Article
records and makes this list available to the home.html
template. This pattern is enabled by the Marten::Handlers::RecordList
generic handler. In order to use it, let's modify the src/handlers/home_handler.cr
file as follows:
class HomeHandler < Marten::Handlers::RecordList
model Article
template_name "home.html"
list_context_name "articles"
end
In the above snippet we use a few class methods in order to define how the handler should behave: #model
allows to define the model class that should be used to retrieve the record, #template_name
allows to define the name of the template to render, and #list_context_name
allows to define the name of the record list variable in the template context.
Let's continue with the ArticleDetailHandler
class: this handler retrieves a specific Article
record from a pk
route parameter, and "renders" it using a specific template. This pattern is enabled by the Marten::Handlers::RecordDetail
generic handler. In order to use it, let's modify the src/handlers/article_detail_handler.cr
file as follows:
class ArticleDetailHandler < Marten::Handlers::RecordDetail
model Article
template_name "article_detail.html"
record_context_name "article"
end
In order to configure how the handler should behave, we make use of a few class methods here as well: #model
allows to define the model class of the record that should be retrieved, #template_name
defines the template to render, and #record_context_name
defines the name of the record variable in the template context.
Now let's look at the ArticleCreateHandler
class: this class displays a form when processing GET requests, and it validates a schema that is used to create a specific record when processing POST requests. This exact pattern is enabled by the Marten::Handlers::RecordCreate
generic handler. In order to use it, we can modify the src/handlers/article_create_handler.cr
file as follows:
class ArticleCreateHandler < Marten::Handlers::RecordCreate
model Article
schema ArticleSchema
template_name "article_create.html"
success_route_name "home"
end
Here, #model
allows to define the model class to use to create the new record, #schema
is the schema class that should be used to validated the incoming data, #template_name
defines the name of the template to render, and #success_route_name
is the name of the route to redirect to after a successful record creation.
We can now look at the ArticleUpdateHandler
class: this class retrieves a specific record and displays a form when processing GET requests, and it validates a schema whose data is used to update the record when processing POST requests. This pattern is enabled by the Marten::Handlers::RecordUpdate
generic handler. Let's use it and let's modify the src/handlers/article_update_handler.cr
file as follows:
class ArticleUpdateHandler < Marten::Handlers::RecordUpdate
model Article
schema ArticleSchema
template_name "article_update.html"
success_route_name "home"
record_context_name "article"
end
Here, #model
allows to define the model class to use to retrieve and update the record, #schema
is the schema class that should be used to validated the incoming data, #template_name
defines the name of the template to render, #success_route_name
is the name of the route to redirect to after a successful record update, and #record_context_name
is the name of the record variable in the template context.
Finally, let's look at the ArticleDeleteHandler
class: this handler renders a template when processing GET requests, and performs the deletion of the considered record when processing POST requests. This pattern is provided by the Marten::Handlers::RecordDelete
generic handler. In order to use it, let's modify the src/handlers/article_delete_handler.cr
file as follows:
class ArticleDeleteHandler < Marten::Handlers::RecordDelete
model Article
template_name "article_delete.html"
success_route_name "home"
record_context_name "article"
end
In order to configure how the handler should behave, we make use of a few class methods here as well: #model
allows to define the model class of the record that should be retrieved and deleted, #template_name
defines the template to render, and #success_route_name
defines the name of the route to redirect to once the record is deleted.
Now if you go to your application again at http://localhost:8000, you will notice that everything is working like it used to do before we introduced these changes (but with less code!).
Please refer to Generic handlers to learn more about generic handlers.
What's next?
As part of this tutorial, we covered the main features of the Marten web framework by implementing a very simple application:
- we learned to define models in order to interact with the database
- we learned to create handlers and to map URLs to these in order to process HTTP requests
- we learned to render templates in order to define the presentation logic of an application
Now that you've experimented with these core concepts of the framework, you should not hesitate to update the application we just created in order to experiment further and add new features to it.
The Marten documentation also contains plenty of additional guides allowing you to keep exploring and learning more about other areas of the framework. These may be useful depending on the specific needs of your application: Testing, Applications, Security, Internationalization, etc.