soil
Version, currently master branch1 version
- master branchlatestJul 1, 2019
github.com/joaodiogocosta/soil
A Web Framework written in Crystal optimized for productivity and extensibility
11 stars
0 dependents
License: MIT
Installation
# Add this to your shard.yml
dependencies:
soil:
github: joaodiogocosta/soil
branch: mastermaster is a branch, not a release, so this tracks it rather than pinning a version.
Then run:
shards installshard.yml
- Crystal
- no constraint declared
- License
- MIT
- Author
- João Diogo Costa
Dependencies
This version declares no dependencies.
README
# Soil
Soil is a Web Framework written in Crystal optimized for productivity and extensibility.
[](https://travis-ci.org/joaodiogocosta/soil)
It is inspired by the most well known Ruby and Javascript web frameworks, inheriting the completeness of Ruby on Rails, the clarity and readability of ExpressJS and the simplicity of Sinatra.
## Current state
Soil is still a work in progress with a lot of missing features, but it will be constantly updated until it reaches a stable version.
## Installation
Add Soil to `shard.yml`:
```yaml
dependencies:
soil:
github: joaodiogocosta/soil
branch: master
```
And then run:
```
$ crystal deps
```
## Basic Example
The following is a basic example that defines a `GET /posts` endpoint and responds with JSON.
```crystal
# blog_app.cr
class BlogApp < Soil::App
get "posts" do |req, res|
posts = [...]
res.json(posts)
end
end
BlogApp.new.run
```
And then run:
```
$ crystal blog_app.cr
```
## Table of Contents
* [Routes](#routes)
* [Definition](#definition)
* [Handlers](#handlers)
* [Params](#params)
* [Response](#response)
* [JSON](#json)
* [Render Templates](#render-templates)
* [Inline HTML](#inline-html)
* [Text](#text)
* [Redirect](#redirect)
* [Hooks](#hooks)
* [Namespaces](#namespaces)
* [Nested Routes](#nested-routes)
* [Views and Layouts](#views-and-layouts)
* [Static Files](#static-files)
* [Configuration](#configuration)
## Routes
### Definition
Defining routes is extremely easy.
First, define a class that acts as a routes container, such as a Controller does in other web frameworks, and then define an arbitrary amount of routes by using methods such as `get` and passing a block handler that accepts `req` (request) and `res` (response) as arguments.
```crystal
class BlogApp < Soil::App
get "/" do |req, res|
# ...
end
post "/" do |req, res|
# ...
end
end
```
### Handlers
A Route accepts two different types of handlers, crystal's built-in [Proc](https://crystal-lang.org/docs/syntax_and_semantics/literals/proc.html) and [Action](#action).
#### Proc
A `Proc` is the simplest handler. To use it, simply pass in a block that accepts `req` (request) and `res` (response) as arguments:
```crystal
class BlogApp < Soil::App
get "/" do |req, res|
# ...
end
end
```
#### Action
An `Action` is a plain Crystal object that includes `Soil::Action`, which is a module that ensures a proper `call` method is implemented:
```crystal
class PostsIndexAction
include Soil::Action
def call(req, res)
posts = [...]
res.json(posts)
end
end
class PostsApp < Soil::App
get "/", PostsIndexAction.new
end
```
This is particularly useful for endpoints that incorporate a considerable amount of operations, such as an enpoint that creates a new User account:
```crystal
class CreateUserAction
include Soil::Action
def initialize
@email_sender = EmailSender.new
end
def call(req, res)
user = create_user(req.params["user"])
if user
send_welcome_email(user)
res.json(user)
else
res.status_code = 400
res.json("message" => "Could not create User")
end
end
private def create_user(attributes)
User.create(attributes)
end
private def send_welcome_email(user)
@email_sender.send(:welcome_email, user.email)
end
end
```
And then use it in a Route:
```crystal
class BlogApp < Soil::App
get "/", CreateUserAction.new
end
```
#### Multiple Handlers
Routes can have multiple handlers. `Proc` and `Action` can be combined in arrays:
```crystal
class LogAction
include Soil::Action
def call(req, res)
# Log something to STDOUT
end
end
class PostsIndexAction
include Soil::Action
def call(req, res)
posts = [...]
res.json(posts)
end
end
class PostsApp < Soil::App
get "/", [
LogAction.new,
PostsIndexAction.new,
-> (req : Soil::Http::Request, res : Soil::Http::Response) {
# ...
}]
end
```
### Params
#### URL
URL named parameters are captured and accessible via `req.params.url`:
```crystal
class BlogApp < Soil::App
get "/posts/:post_id/comments/:comment_id" do |req, res|
req.params.url["post_id"] # => "45"
req.params.url["comment_id"] # => "87"
end
end
```
#### Query
Query parameters are accessible via `req.params.query`:
```crystal
class BlogApp < Soil::App
get "/posts?search=crystal" do |req, res|
req.params.query["search"] # => "crystal"
end
end
```
#### JSON
JSON parameters are captured from the body and accessible via `req.params.json`.
Given the following JSON payload:
```json
{
"post": {
"title": "Soil is pure awesomeness!"
}
}
```
Here's how you would access its values:
```crystal
class BlogApp < Soil::App
post "/posts" do |req, res|
req.params.json["post"]["title"] # => "Soil is pure awesomeness!"
end
end
```
### Response
#### JSON
Soil has built-in support for JSON responses. The following will call `to_json` on the object passed in as argument and write to the response body.
```crystal
class Api < Soil::App
get "posts" do |req, res|
posts = [...]
res.json(posts)
end
end
```
This method will set the `Content-Type` header to `application/json`.
#### Render Templates
Render any template (HTML or others) by calling `render` on the response object and passing any `View` class. Read more about views [here](#views-and-layouts).
```crystal
class MyApp < Soil::App
get "/" do |req, res|
res.render(IndexView)
end
end
```
This method will set the `Content-Type` header to `text/html`.
#### Inline HTML
For inline HTML:
```crystal
class HtmlApp < Soil::App
get "/" do |req, res|
res.html("<html></html>")
end
end
```
This method will set the `Content-Type` header to `text/html`.
#### Text
For simple plain text responses:
```crystal
class TextApp < Soil::App
get "/" do |req, res|
res.text("Soil says hi!")
end
end
```
This method will set the `Content-Type` header to `text/plain`.
#### Redirect
Use `redirect` to redirect the request to another path or url:
```crystal
class TextApp < Soil::App
get "/" do |req, res|
res.redirect("/login")
# or
res.redirect("http://google.pt")
end
end
```
This method will set the status code to `302` or `303` for `GET` and `POST` requests, respectively.
### Hooks
Hooks are evaluated before or after each request within the same context of the request. They run before/after every request defined within the App or within mounted Apps.
```crystal
class Posts < Soil::App
before do |req, res|
pp req.params # print params before handler
end
after do |req, res|
pp res.status_code # print status code after handler
end
# ...
end
```
### Namespaces
The following wil prepend `blog` to all routes, hence `/blog/posts`:
```crystal
class BlogApp < Soil::App
namespace "blog"
get "posts" do |req, res|
posts = [...]
res.json(posts)
end
end
```
Namespaces are propagated to all routes.
### Nested Routes
Nested Routes are the foundation of Soil's extensibility features.
Soil Apps can be combined to create complex applications of multiple endpoints with multiple levels of nesting.
```crystal
class Users < Soil::App
get "/" do |req, res|
users = [...]
res.json(users)
end
end
class Comments < Soil::App
get "/" do |req, res|
# ...
req.params["post_id"] => # 231
# ...
end
end
class Posts < Soil::App
mount ":post_id/comments", CommentsApp
get "/" do |req, res|
posts = [...]
res.json(posts)
end
end
class BlogApp < Soil::App
namespace "blog"
mount "/users", UsersApp
mount "/posts", PostsApp
end
```
This will generate the following routes:
`GET /blog/users`
`GET /blog/posts`
`GET /blog/posts/:post_id/comments`
## Views and Layouts
Views and templating engines are a crucial part of every web framework. Soil's approach is to provide enough flexibility to the developer while enforcing good practices, such as having a reasonably well-defined data structure.
### The easy way, ideal for simple templates
Soil has a built-in helper to render templates. It is available in regular routes and also in any Action subclasses.
```crystal
class MyApp < Soil::App
get "/" do |req, res|
render_template res, "index.html.ecr"
end
end
```
Then create a new file which is the actual template. Soil uses Crystal's built-in templating engine [ECR](https://crystal-lang.org/api/ECR.html):
```html
(index.html.ecr)
<p>I Love <%= name %>!</p>
```
### The preferred way, ideal for templates with dynamic data
Declare a new class that will act as the data container for the view template:
```crystal
class IndexView
include Soil::View
def initialize(data)
@name = data["name"]
end
def name
@name
end
def render(io : IO)
render_template io, "index.html.ecr"
end
end
```
It's mandatory to implement a `render(io : IO)` method. You can return whatever you want, in this case we will return a pre-compiled template.
Views are plain Crystal objects, you are free to implement them as you find more suitable to your needs, just remember to make publicly accessible the methods that are used in the template (`name` in this case).
Ultimately, reference it in your request handler by calling `render` just like any `text` or `JSON` response:
```crystal
class MyApp < Soil::App
get "/" do |req, res|
res.render(IndexView, { "name" => "Crystal" })
end
end
```
The result will be:
```
<p>I love Crystal!<p>
```
### Layouts
Soil supports View Layouts.
Given a layout template, add the placeholder `yield_contents` to where you want to render a nested template:
```html
(layout.html.ecr)
<div>
<%= yield_contents %>
</div>
```
And then reference this file in the View:
```crystal
class IndexView
include Soil::View
def render(io : IO)
render_template io, "index.html.ecr", layout: "layout.html.ecr"
end
end
```
The contents of `index.html.ecr` will replace `<%= yield_contents %>` in the layout.
## Static Files
To serve static files just enable the following configuration option:
```crystal
class MyApp < Soil::App
configure do |config|
config.serve_static_files = true
end
end
```
This will serve files inside `public/`.
If you want to change the public directory from which the files are served, use the configuration option `public_dir`:
```crystal
config.public_dir = "custom_dir"`
```
## Configuration
As of now, Soil accepts configuration options for the following parameters:
```crystal
class BlogApp < Soil::App
configure do |config|
# Binding
config.host = "127.0.0.1"
config.port = "4000"
# Static Files
config.serve_static_files = false
config.public_dir = "public"
end
# ...
end
```
## Requirements
Crystal - Please refer to http://crystal-lang.org/docs/installation for instructions for your operating system.
## Contributing
Everyone is invited to make Soil a better project:
1. Fork it ( https://github.com/joaodiogocosta/soil/fork )
2. Create your feature branch (git checkout -b my-new-feature)
3. Commit your changes (git commit -am 'Add some feature')
4. Push to the branch (git push origin my-new-feature)
5. Create a new Pull Request
## License
MIT License
Copyright (c) 2017 João Diogo Costa
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Documentation
Built from the current release. The first visit to a release nobody has asked for starts its build.
Links
This branch
- Branch
master- Seen
- Jul 1, 2019
- Indexed
- yes
Dependents
No indexed shard depends on this one yet.
Repository
github.com/joaodiogocosta/soil
Metadata
- Created
- Aug 12, 2026
- Updated
- Aug 12, 2026
- Synced
- Aug 12, 2026
- Versions
- 1