Tarsier 🐒
A modern, high-performance Ruby web framework designed for speed, simplicity, and developer productivity.
Tarsier delivers sub-millisecond routing through a compiled radix tree architecture while maintaining an intuitive API that feels natural to Ruby developers.
🚀 Quick Start
# Install Tarsier
gem install tarsier
# Create a new app
tarsier new my_app
cd my_app
# Start the server
tarsier server
# => Listening on http://localhost:7827Or create a minimal app:
# app.rb
require 'tarsier'
app = Tarsier.app do
get('/') { { message: 'Hello, World!' } }
get('/users/:id') { |req| { user_id: req.params[:id] } }
end
run app📊 Performance Highlights
- 700,000+ routes per second
- Sub-millisecond response times
- Zero runtime dependencies
- 100% type coverage with RBS
Table of Contents
- Quick Start
- Features
- Requirements
- Installation
- Command Line Interface
- Routing
- Controllers
- Request and Response
- Middleware
- Database
- WebSocket and SSE
- Models
- Configuration
- Testing
- Benchmarks
- Community
- Contributing
- License
Features
- ⚡ High Performance: Compiled radix tree router achieving 700,000+ routes per second
- 📦 Zero Runtime Dependencies: Core framework operates without external dependencies
- 🗄️ Multi-Database Support: Built-in support for SQLite, PostgreSQL, and MySQL
- 💎 Modern Ruby: Built for Ruby 3.3+ with YJIT and Fiber scheduler support
- 🔄 Async Native: First-class support for Fiber-based concurrency patterns
- 📝 Type Safety Ready: Complete RBS type signatures for static analysis
- 🚀 Developer Experience: Intuitive APIs, detailed error messages, minimal boilerplate
- 🔒 Production Ready: Built-in middleware for CORS, CSRF, rate limiting, and compression
Requirements
- Ruby 3.3 or higher
- Rack 3.0+ (for Rack compatibility layer)
Installation
Add Tarsier to your application's Gemfile:
gem 'tarsier'Then execute:
bundle installOr install globally for CLI access:
gem install tarsier📖 Need help getting started? Check out our comprehensive documentation website with step-by-step guides, examples, and API reference.
Command Line Interface
Tarsier provides a comprehensive CLI for project scaffolding and development workflows.
Creating Applications
# Standard application with views
tarsier new my_app
# API-only application (no view layer)
tarsier new my_api --api
# Minimal application structure
tarsier new my_app --minimal
# Skip optional setup steps
tarsier new my_app --skip-git --skip-bundleCode Generation
# Generate a controller with actions
tarsier generate controller users index show create update destroy
# Generate a model with attributes
tarsier generate model user name:string email:string created_at:datetime
# Generate a complete resource (model + controller + routes)
tarsier generate resource post title:string body:text published:boolean
# Generate custom middleware
tarsier generate middleware authentication
# Generate a database migration
tarsier generate migration create_users name:string email:string
tarsier generate migration add_role_to_usersShorthand aliases are available:
tarsier g controller users index show
tarsier g model user name:stringDevelopment Server
# Start with defaults (port 7827)
tarsier server
# Custom port
tarsier server -p 4000
# Specify environment
tarsier server -e production
# Bind to specific host
tarsier server -b 127.0.0.1Interactive Console
# Development console
tarsier console
# Production console
tarsier console -e productionRoute Inspection
# Display all routes
tarsier routes
# Filter routes by pattern
tarsier routes -g users
# Output as JSON
tarsier routes --jsonVersion Information
tarsier versionRouting
Tarsier uses a compiled radix tree router optimized for high-throughput applications.
Basic Routes
Tarsier.routes do
get '/users', to: 'users#index'
post '/users', to: 'users#create'
get '/users/:id', to: 'users#show'
put '/users/:id', to: 'users#update'
delete '/users/:id', to: 'users#destroy'
endRoute Parameters
Tarsier.routes do
# Required parameters
get '/users/:id', to: 'users#show'
# Multiple parameters
get '/posts/:post_id/comments/:id', to: 'comments#show'
# Wildcard parameters
get '/files/*path', to: 'files#show'
# Parameter constraints
get '/posts/:id', to: 'posts#show', constraints: { id: /\d+/ }
endRESTful Resources
Tarsier.routes do
# Full resource routes
resources :posts
# Nested resources
resources :posts do
resources :comments
end
# Limit actions
resources :posts, only: [:index, :show]
resources :posts, except: [:destroy]
# Singular resource
resource :profile
endNamespaces and Scopes
Tarsier.routes do
# Namespace (affects path and controller lookup)
namespace :admin do
resources :users
end
# Routes: /admin/users -> Admin::UsersController
# Scope (affects path only)
scope path: 'api/v1' do
resources :users
end
# Routes: /api/v1/users -> UsersController
endNamed Routes
Tarsier.routes do
get '/login', to: 'sessions#new', as: :login
get '/logout', to: 'sessions#destroy', as: :logout
root to: 'home#index'
end
# Generate paths
app.path_for(:login) # => "/login"
app.path_for(:user, id: 123) # => "/users/123"Controllers
Controllers handle request processing with support for filters, parameter validation, and multiple response formats.
Basic Controller
class UsersController < Tarsier::Controller
def index
users = User.all
render json: users
end
def show
user = User.find(params[:id])
render json: user
end
def create
user = User.create(params.permit(:name, :email))
render json: user, status: 201
end
endFilters
class UsersController < Tarsier::Controller
before_action :authenticate
before_action :load_user, only: [:show, :update, :destroy]
after_action :log_request
def show
render json: @user
end
private
def authenticate
head 401 unless current_user
end
def load_user
@user = User.find(params[:id])
end
def log_request
Logger.info("Processed #{action_name}")
end
endParameter Validation
class UsersController < Tarsier::Controller
params do
requires :name, type: String, min: 2, max: 100
requires :email, type: String, format: URI::MailTo::EMAIL_REGEXP
optional :role, type: String, in: %w[user admin], default: 'user'
optional :age, type: Integer, greater_than: 0
end
def create
# validated_params contains coerced and validated parameters
user = User.create(validated_params)
render json: user, status: 201
end
endException Handling
class ApplicationController < Tarsier::Controller
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ValidationError, with: :unprocessable
private
def not_found(exception)
render json: { error: 'Resource not found' }, status: 404
end
def unprocessable(exception)
render json: { errors: exception.errors }, status: 422
end
endResponse Streaming
class ReportsController < Tarsier::Controller
def export
response.stream do |out|
User.find_each do |user|
out << user.to_csv
out << "\n"
end
end
end
endRequest and Response
Request Object
The request object provides immutable access to request data with lazy parsing for optimal performance.
# HTTP method and path
request.method # => :GET
request.path # => '/users/123'
request.url # => 'https://example.com/users/123'
# Parameters
request.params # Combined route, query, and body params
request.query_params # Query string parameters only
request.body_params # Parsed body parameters
request.route_params # Parameters from route matching
# Headers and metadata
request.headers # All HTTP headers
request.header('Authorization') # Specific header
request.content_type # Content-Type header
request.accept # Accept header
request.user_agent # User-Agent header
# Client information
request.ip # Client IP address
request.cookies # Parsed cookies
# Request type checks
request.get? # => true/false
request.post? # => true/false
request.json? # Content-Type includes application/json
request.xhr? # X-Requested-With: XMLHttpRequest
request.secure? # HTTPS requestResponse Object
# Set status and headers
response.status = 201
response.set_header('X-Custom-Header', 'value')
response.content_type = 'application/json'
# Response helpers
response.json({ data: users })
response.html('<h1>Hello</h1>')
response.text('Plain text response')
response.redirect('/new-location', status: 302)
# Cookies
response.set_cookie('session', token,
http_only: true,
secure: true,
same_site: 'Strict',
max_age: 86400
)
response.delete_cookie('session')
# Streaming
response.stream do |out|
out << 'chunk 1'
out << 'chunk 2'
endMiddleware
Tarsier includes production-ready middleware and supports custom middleware development.
Built-in Middleware
app = Tarsier.application do
# Request logging
use Tarsier::Middleware::Logger
# CORS handling
use Tarsier::Middleware::CORS,
origins: ['https://example.com'],
methods: %w[GET POST PUT DELETE],
credentials: true
# CSRF protection
use Tarsier::Middleware::CSRF,
skip: ['/api/webhooks']
# Rate limiting
use Tarsier::Middleware::RateLimit,
limit: 100,
window: 60
# Response compression
use Tarsier::Middleware::Compression,
min_size: 1024
# Static file serving
use Tarsier::Middleware::Static,
root: 'public',
cache_control: 'public, max-age=31536000'
endCustom Middleware
class TimingMiddleware < Tarsier::Middleware::Base
def call(request, response)
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@app.call(request, response)
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
response.set_header('X-Response-Time', "#{(duration * 1000).round(2)}ms")
response
end
endMiddleware Stack Management
app.middleware_stack.insert_before(Tarsier::Middleware::Logger, CustomMiddleware)
app.middleware_stack.insert_after(Tarsier::Middleware::CORS, AnotherMiddleware)
app.middleware_stack.delete(Tarsier::Middleware::Static)
app.middleware_stack.swap(OldMiddleware, NewMiddleware)Database
Tarsier includes a lightweight database layer with support for SQLite, PostgreSQL, and MySQL.
Connecting to a Database
# SQLite
Tarsier.db :sqlite, 'db/app.db'
Tarsier.db :sqlite, ':memory:' # In-memory database
# PostgreSQL
Tarsier.db :postgres, 'postgres://localhost/myapp'
Tarsier.db :postgres, host: 'localhost', database: 'myapp', username: 'user'
# MySQL
Tarsier.db :mysql, host: 'localhost', database: 'myapp', username: 'root'Raw Queries
# Execute queries
Tarsier.database.execute('SELECT * FROM users WHERE active = ?', true)
# Get single result
user = Tarsier.database.get('SELECT * FROM users WHERE id = ?', 1)
# Insert and get ID
id = Tarsier.database.insert(:users, name: 'John', email: 'john@example.com')
# Update records
Tarsier.database.update(:users, { active: false }, id: 1)
# Delete records
Tarsier.database.delete(:users, id: 1)Transactions
Tarsier.database.transaction do
Tarsier.database.insert(:accounts, user_id: 1, balance: 100)
Tarsier.database.update(:users, { has_account: true }, id: 1)
end
# Automatically rolls back on errorMigrations
Generate a migration:
tarsier generate migration create_users name:string email:stringMigration file:
class CreateUsers < Tarsier::Database::Migration
def up
create_table :users do |t|
t.string :name
t.string :email, null: false
t.boolean :active, default: true
t.timestamps
end
add_index :users, :email, unique: true
end
def down
drop_table :users
end
endRun migrations:
Tarsier.database.migrate # Run pending migrations
Tarsier.database.migrate(:down) # Rollback migrationsSupported Column Types
| Type | SQL Type |
|---|---|
| string | VARCHAR(255) |
| text | TEXT |
| integer | INTEGER |
| bigint | BIGINT |
| float | REAL |
| decimal | DECIMAL |
| boolean | BOOLEAN |
| date | DATE |
| datetime | DATETIME |
| timestamp | TIMESTAMP |
| time | TIME |
| binary | BLOB |
| json | JSON |
WebSocket and SSE
WebSocket Support
class ChatSocket < Tarsier::WebSocket
on :connect do
subscribe "room:#{params[:room_id]}"
broadcast "room:#{params[:room_id]}", { type: 'join', user: current_user }
end
on :message do |data|
broadcast "room:#{params[:room_id]}", {
type: 'message',
user: current_user,
content: data
}
end
on :close do
broadcast "room:#{params[:room_id]}", { type: 'leave', user: current_user }
end
endServer-Sent Events
class EventsController < Tarsier::Controller
def stream
sse = Tarsier::SSE.new(request, response)
sse.start
loop do
sse.send({ time: Time.now.iso8601 }, event: 'tick', id: SecureRandom.uuid)
sleep 1
end
rescue IOError
sse.close
end
endModels
Tarsier includes a lightweight ORM with Active Record-style patterns. Models integrate seamlessly with the database layer.
Model Definition
class User < Tarsier::Model
table :users
attribute :name, :string
attribute :email, :string
attribute :active, :boolean, default: true
attribute :created_at, :datetime
validates :name, presence: true, length: { minimum: 2, maximum: 100 }
validates :email, presence: true, format: /@/
has_many :posts
has_one :profile
belongs_to :organization
endCRUD Operations
# Create
user = User.create(name: 'John', email: 'john@example.com')
user = User.new(name: 'Jane')
user.save
# Read
User.find(1)
User.find!(1) # Raises RecordNotFoundError if not found
User.find_by(email: 'john@example.com')
User.all
# Update
user.update(name: 'John Doe')
user.name = 'Johnny'
user.save
# Delete
user.destroyQuery Interface
# Chainable query methods
User.where(active: true)
.where_not(role: 'guest')
.order(created_at: :desc)
.limit(10)
.offset(20)
# Range queries
User.where(age: 18..65)
# IN queries
User.where(status: ['active', 'pending'])
# Select specific columns
User.select(:id, :name, :email)
# Pluck values
User.where(active: true).pluck(:email)
User.pluck(:id) # Same as User.ids
# Joins
User.joins(:posts, on: 'users.id = posts.user_id')
# Eager loading
User.includes(:posts, :profile)
# Aggregations
User.count
User.where(active: true).count
User.exists?(email: 'test@example.com')
# Batch operations
User.where(inactive: true).update_all(archived: true)
User.where(archived: true).delete_all
# Find or create
User.where(email: 'new@example.com').find_or_create_by({ email: 'new@example.com' })
User.where(email: 'new@example.com').find_or_initialize_by({ email: 'new@example.com' })Validations
class Post < Tarsier::Model
attribute :title, :string
attribute :body, :text
attribute :status, :string
validates :title, presence: true, length: { minimum: 5, maximum: 200 }
validates :status, inclusion: { in: %w[draft published archived] }
end
post = Post.new(title: 'Hi')
post.valid? # => false
post.errors # => { title: ["is too short (minimum 5)"] }Associations
class Post < Tarsier::Model
belongs_to :user
has_many :comments
end
class Comment < Tarsier::Model
belongs_to :post
belongs_to :user
end
# Access associations
post = Post.find(1)
post.user # => User instance
post.comments # => Array of Comment instancesConfiguration
Tarsier.application do
configure do
# Security
self.secret_key = ENV.fetch('SECRET_KEY')
self.force_ssl = true
# Server
self.host = '0.0.0.0'
self.port = 3000
# Application
self.log_level = :info
self.default_format = :json
self.static_files = true
end
endEnvironment-Specific Configuration
Tarsier.application do
configure do
case Tarsier.env
when 'production'
self.force_ssl = true
self.log_level = :warn
when 'development'
self.log_level = :debug
when 'test'
self.log_level = :error
end
end
endTesting
Tarsier applications are designed for testability with RSpec integration.
Setup
# spec/spec_helper.rb
require 'tarsier'
require 'rack/test'
RSpec.configure do |config|
config.include Rack::Test::Methods
def app
Tarsier.app
end
endController Tests
RSpec.describe UsersController do
describe 'GET /users' do
it 'returns users list' do
get '/users'
expect(last_response.status).to eq(200)
expect(JSON.parse(last_response.body)).to have_key('users')
end
end
describe 'POST /users' do
it 'creates a user' do
post '/users', { name: 'John', email: 'john@example.com' }.to_json,
'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq(201)
end
end
endRunning Tests
bundle exec rspec
bundle exec rspec spec/controllers
bundle exec rspec --format documentationBenchmarks
Run the included benchmark suite:
bundle exec rake benchmark
bundle exec rake benchmark:router
bundle exec rake benchmark:requestPerformance Characteristics
| Operation | Throughput |
|---|---|
| Static route matching | 700,000+ req/sec |
| Parameterized routes | 250,000+ req/sec |
| Nested resources | 115,000+ req/sec |
| Request parsing | 700,000+ req/sec |
Benchmarks performed on Ruby 3.3 with YJIT enabled.
Community
Join the Tarsier community to get help, share ideas, and contribute to the project:
- 💬 Discord: Join our Discord server for real-time discussions and support
- � Documentation: Complete documentation website with guides and examples
- � *Issues: Report bugs or request features on GitHub
- 💎 RubyGems: View package details and installation stats
Getting Help
- Check the documentation for comprehensive guides
- Join our Discord community for quick questions and discussions
- Search existing issues before creating new ones
- Follow the contributing guidelines when submitting PRs
Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/improvement) - Commit your changes (
git commit -am 'Add new feature') - Push to the branch (
git push origin feature/improvement) - Create a Pull Request
Development Setup
git clone https://github.com/tarsier-rb/tarsier.git
cd tarsier
bundle install
bundle exec rspecCode Quality
bundle exec rubocop
bundle exec yard docLicense
Tarsier is released under the MIT License.
Acknowledgments
Tarsier draws inspiration from the Ruby web framework ecosystem, particularly Rails, Sinatra, and Roda, while pursuing its own vision of performance and simplicity.
Special thanks to our community members and contributors who help make Tarsier better every day. Join us on Discord to be part of the journey!
