Project

tarsier

0.0
No release in over 3 years
Tarsier is a fast, clean Ruby web framework with async support, compiled routing, and zero runtime dependencies.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

~> 3.0
~> 2.1
~> 13.0
~> 3.12
~> 1.50
~> 0.2
~> 0.9

Runtime

>= 0
 Project Readme

Tarsier 🐒

Tarsier Logo

A modern, high-performance Ruby web framework designed for speed, simplicity, and developer productivity.

Ruby License Discord


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:7827

Or 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 install

Or 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-bundle

Code 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_users

Shorthand aliases are available:

tarsier g controller users index show
tarsier g model user name:string

Development 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.1

Interactive Console

# Development console
tarsier console

# Production console
tarsier console -e production

Route Inspection

# Display all routes
tarsier routes

# Filter routes by pattern
tarsier routes -g users

# Output as JSON
tarsier routes --json

Version Information

tarsier version

Routing

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'
end

Route 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+/ }
end

RESTful 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
end

Namespaces 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
end

Named 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
end

Filters

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
end

Parameter 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
end

Exception 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
end

Response 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
end

Request 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 request

Response 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'
end

Middleware

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'
end

Custom 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
end

Middleware 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 error

Migrations

Generate a migration:

tarsier generate migration create_users name:string email:string

Migration 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
end

Run migrations:

Tarsier.database.migrate        # Run pending migrations
Tarsier.database.migrate(:down) # Rollback migrations

Supported 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
end

Server-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
end

Models

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
end

CRUD 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.destroy

Query 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 instances

Configuration

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
end

Environment-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
end

Testing

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
end

Controller 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
end

Running Tests

bundle exec rspec
bundle exec rspec spec/controllers
bundle exec rspec --format documentation

Benchmarks

Run the included benchmark suite:

bundle exec rake benchmark
bundle exec rake benchmark:router
bundle exec rake benchmark:request

Performance 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:

Getting Help


Contributing

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/improvement)
  3. Commit your changes (git commit -am 'Add new feature')
  4. Push to the branch (git push origin feature/improvement)
  5. Create a Pull Request

Development Setup

git clone https://github.com/tarsier-rb/tarsier.git
cd tarsier
bundle install
bundle exec rspec

Code Quality

bundle exec rubocop
bundle exec yard doc

License

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!