Over the course of my career, I used to work extensively with two web frameworks: Django and Ruby on Rails. These two frameworks are regularly compared, and I can't even imagine the number of articles or discussions that touched on this subject over the years. The bottom line is that both frameworks have their strengths: in my opinion, Django shines with its design decisions and the general assumptions it makes about things like models, migrations, templates, routing, etc. Rails shines with its awesome DSLs (like callbacks, or validations) and the overall productivity it enables.
A few years ago, I was trying Sorbet for the first time and I was realizing how frustrating the developer experience provided by this tool was: horrible syntax with sig extensions (and duplicated method definitions), magic comments, separate RBI files generated from the codebase, ... Don't get me wrong: Sorbet certainly fulfills its mission of bringing typing to the Ruby ecosystem, and enabling most of the benefits that come with typed languages. But unfortunately, this is to the detriment of the developer experience because all of this makes Ruby lose its beauty and slickness.
Developer experience matters. And well, while I was reading more on Sorbet, I stumbled upon a language called Crystal. The timing couldn't have been better: Crystal - heavily inspired by Ruby's syntax - provided exactly the kind of typing syntax that I would've loved to rely on in a "typed" Ruby: elegant, intuitive, and expressive.
On top of that, Crystal featured a bunch of other very interesting characteristics:
A few years ago, I was trying Sorbet for the first time and I was realizing how frustrating the developer experience provided by this tool was: horrible syntax with sig extensions (and duplicated method definitions), magic comments, separate RBI files generated from the codebase, ... Don't get me wrong: Sorbet certainly fulfills its mission of bringing typing to the Ruby ecosystem, and enabling most of the benefits that come with typed languages. But unfortunately, this is to the detriment of the developer experience because all of this makes Ruby lose its beauty and slickness.
Developer experience matters. And well, while I was reading more on Sorbet, I stumbled upon a language called Crystal. The timing couldn't have been better: Crystal - heavily inspired by Ruby's syntax - provided exactly the kind of typing syntax that I would've loved to rely on in a "typed" Ruby: elegant, intuitive, and expressive.
On top of that, Crystal featured a bunch of other very interesting characteristics:
- A powerful compilation and the ability to catch typing errors at compile time
- Meta-programming capabilities with macros
- Very good performances (in a lot of benchmarks, Crystal regularly beats other compiled languages like Go or Elixir)
I literally fell in love with the language and started playing with it regularly. Soon I started playing with the existing options in terms of high-level web frameworks (mainly Amber and Lucky). I thought it would be fun to create a web framework that enabled design decisions similar to Django's while also leveraging Crystal's syntax and performances to enable an enjoyable and productive developer experience. That's how I started working on the Marten web framework.
What is Marten?
Marten is a Crystal Web framework that enables pragmatic development and rapid prototyping. It provides a consistent and extensible set of tools that developers can leverage to build web applications without reinventing the wheel.
Principles
Marten's development has been guided by a few key principles. As it stands, Marten focuses on the following aspects:
- Simple and easy to use: Marten tries to ensure that everything it enables is as simple as possible and that the syntax provided for dealing with the framework's components remains obvious and easy to remember (and certainly not complex or obscure). The framework makes it as easy as possible to leverage its capabilities and perform CRUD operations.
- Full-featured: Marten adheres to the "batteries included" philosophy. Out of the box, it provides the tools and features that are commonly required by web applications: ORM, migrations, translations, templating engine, sessions, and (soon) emailing and authentication.
- Extensible: Marten gives developers the ability to contribute extra functionalities to the framework easily. Things like custom model field implementations, new route parameter types, session stores, etc... can all be registered to the framework easily.
- DB-Neutral: The framework's ORM is usable with multiple database backends (including MySQL, PostgreSQL, and SQLite).
- App-oriented: Marten allows separating projects into a set of logical "apps", which helps improve code organization and makes it easy for multiple developers to work on different components. Each app can contribute specific abstractions and features to a project like models and migrations, templates, HTTP handlers and routes, etc. These apps can also be extracted in Crystal shards in order to contribute features and behaviours to other Marten projects. The goal behind this capability is to allow the creation of a powerful apps ecosystem over time and to encourage "reusability" and "pluggability"
- Backend-oriented: The framework is intentionally very "backend-oriented" because the idea is to not make too many assumptions regarding how the frontend code and assets should be structured, packaged or bundled together. The framework can't account for all the ways assets can be packaged and/or bundled together and does not advocate for specific solutions in this area. Some projects might require a webpack strategy to bundle assets, some might require a fingerprinting step on top of that, and others might need something entirely different. How these toolchains are configured or set up is left to the discretion of web application developers, and the framework simply makes it easy to reference these assets and collect them at deploy time to upload them to their final destination
What does it look like?
As mentioned previously, Marten provides support for many key components in order to help developers build web applications in a productive way. The most important ones certainly are models, templates, and handlers.
class Article < Marten::Model field :id, :big_int, primary_key: true, auto: true field :title, :string, max_size: 128 field :content, :text field :author, :many_to_one, to: User end
Marten’s vision when it comes to models is that everything needed to understand a model should be defined within the model itself. In this light, Marten’s models drive tables (and not the reverse way around) and migrations are generated automatically from model definitions. Migrations can still be written manually using a convenient DSL if necessary, but the idea is that defining a model and its fields is all that is necessary in order to have a corresponding table taken care of automatically by the framework. This is a very convenient mechanism: most Crystal web frameworks require that you write the migrations corresponding to your model definitions manually; being able to rely on an automatic mechanism that does this for you by default is certainly a plus.
As highlighted in the above snippet, models explicitly define "fields" and relationships through the use of a unique and simple field macro. These fields can contribute database columns to the model table and they can be queried through the use of an automatically-generated database access API. One interesting aspect of model fields is that they operate at a higher level than columns: they can actually contribute features or validations to your models and they don't have to correspond to primitive types only. For example, an email model field could ensure that only email address values are allowed for a specific field while also contributing a string column at the database level.
Templates let you define your presentation logic:
{% extend "base.html" %} {% block content %} <ul> {% for article in articles %} <li>{{ article.title }}</li> {% endfor %} </ul> {% endblock content %}
Templates provide a convenient way of defining your presentation logic and writing contents (such as HTML) that are rendered dynamically. These renderings can involve model records or any other variables needed for the templates' requirements.
Marten's templates provide a Jinja-like syntax where you can use dynamic variables as well as some programming constructs and leverage relatively common patterns such as template tags or filters. This syntax is relatively common now and has been used in a lot of projects such as Django, Liquid, or Nunjucks. The advantage of relying on a dedicated templating engine is that templates don't assume Crystal knowledge. As such, the framework acknowledges the fact that such templates may be written by developers of various backgrounds that don't necessarily work on the backend of the project.
Moreover, these templates are parsed and rendered at runtime. As such, they are not tied to the compilation process of the project's Crystal binary. This makes them convenient to edit (because they don't require a compilation), which is a plus for projects involving lots of templates. Obviously, the use of these templates (although recommended by the framework) is optional and ECR (Crystal's compiled template language) can technically be used instead.
Handlers let you process HTTP requests:
class ArticleListHandler < Marten::Handler def dispatch render "articles/list.html", { articles: Article.all } end end
Handlers are responsible for processing HTTP requests and for returning HTTP responses. They are the equivalent of a function that would take a request in and that would produce a response out. In the process they can do possibly anything: loading records from the database, rendering HTML templates, producing JSON payloads, ...
Routing in Marten only cares about URL paths. As such the framework does not route based on HTTP methods and it is the responsibility of the mapped handlers to implement the right logic based on the incoming request's verb. For example, a handler might display an HTML page containing a form when processing a GET request, and it might validate possible form data when handling a POST request.
Marten recognizes the existence of common or frequently encountered handler patterns, like the form example I just mentioned above. As such, the framework includes a set of generic handlers that can be leveraged to perform common tasks. These tasks are frequently encountered when working on web applications. For example: displaying a list of records extracted from the database, or deleting a record. Generic handlers take care of these common patterns so that developers - again - don't end up reimplementing the wheel.
Obviously, this only scratches the surface of what's possible with the Marten web framework. You can have a look at the documentation in order to learn more about other components or features such as routing (allowing to map your handlers to URL paths), schemas (allowing to validate request data), file management, internationalization, management commands, security helpers, assets management,...
Get started with Marten
Here are a few ideas on how you can get started with Marten:
- Check out the Marten GitHub repository
- Check out the official documentation
- The installation guide will help you install Crystal and the Marten CLI
- The introduction tutorial will help you discover the main features of the framework by creating a simple web application
- Ask for help or chat with the community in our Discord