Crystal compiles to native machine code but reads like Ruby. Static typing with full type inference catches errors at compile time with no type annotations needed in most cases. Green threads (fibers) communicate through typed channels — Go-style CSP concurrency without callbacks. lib/fun declarations bind directly to C libraries. HTTP::Server handles thousands of concurrent requests with minimal memory. The Spec testing framework matches RSpec syntax. Shards manages dependencies through a simple YAML manifest. Claude Code generates Crystal types, fiber-based concurrent programs, C bindings, HTTP handlers, and test suites for high-performance Crystal services.
CLAUDE.md for Crystal Projects
## Crystal Stack
- Version: Crystal >= 1.13 (stable)
- Concurrency: fibers + channels (CSP) — prefer over threads for I/O-bound work
- HTTP: HTTP::Server with handlers, JSON::Serializable for (de)serialization
- DB: crystal-pg or crystal-sqlite3 with DB::Database + DB::ResultSet
- Testing: Spec — describe/it/expect blocks in spec/ directory
- Build: crystal build --release for production (enables LLVM optimizations)
- Shards: shards.yml for dependencies, `shards install` to resolve
Types and Structs
# src/models/order.cr — Crystal types with type inference
require "json"
require "time"
enum OrderStatus
Pending
Processing
Shipped
Delivered
Cancelled
end
# Struct: value type, stack-allocated, copied on assignment
struct Money
getter cents : Int64
getter currency : String
def initialize(@cents : Int64, @currency : String = "USD")
end
def +(other : Money) : Money
raise ArgumentError.new("Currency mismatch") unless currency == other.currency
Money.new(cents + other.cents, currency)
end
def to_s : String
"$#{(cents / 100.0).round(2)}"
end
def_equals_and_hash cents, currency
end
# Class: reference type, heap-allocated
class Order
include JSON::Serializable
property id : String
property customer_id : String
property status : OrderStatus
property total : Money
property items : Array(OrderItem)
property created_at : Time
property shipped_at : Time? # Nilable type — ? suffix
property tracking_number : String?
def initialize(
@id : String,
@customer_id : String,
@items : Array(OrderItem),
@status : OrderStatus = OrderStatus::Pending,
@created_at : Time = Time.utc
)
@total = items.reduce(Money.new(0)) { |sum, item| sum + item.subtotal }
end
def pending? : Bool
status.pending?
end
def cancelled? : Bool
status.cancelled?
end
def high_value? : Bool
total.cents > 10_000 # Underscores in numeric literals for readability
end
end
struct OrderItem
include JSON::Serializable
getter product_id : String
getter product_name : String
getter quantity : Int32
getter unit_price : Money
def initialize(@product_id, @product_name, @quantity, @unit_price)
end
def subtotal : Money
Money.new(unit_price.cents * quantity)
end
end
Fibers and Channels
# src/worker/order_processor.cr — concurrent processing with fibers
require "channel"
struct JobResult
getter order_id : String
getter success : Bool
getter error : String?
def initialize(@order_id, @success, @error = nil)
end
end
# Process orders concurrently with a worker pool
def process_orders_concurrent(orders : Array(String)) : Array(JobResult)
results_channel = Channel(JobResult).new(orders.size)
semaphore = Channel(Nil).new(10) # Limit to 10 concurrent workers
orders.each do |order_id|
spawn do
semaphore.send(nil) # Acquire slot
result = begin
process_single_order(order_id)
JobResult.new(order_id, success: true)
rescue ex
JobResult.new(order_id, success: false, error: ex.message)
end
results_channel.send(result)
semaphore.receive # Release slot
end
end
# Collect all results
Array(JobResult).new(orders.size) { results_channel.receive }
end
# Pipeline pattern: producer → channel → consumer
def setup_order_pipeline
raw_orders = Channel(String).new(100) # Buffered channel
processed = Channel(JobResult).new(100)
# Producer fiber: reads from queue
spawn do
loop do
order_id = fetch_next_order_from_queue
break if order_id.nil?
raw_orders.send(order_id)
end
raw_orders.close
end
# Worker fibers (pool of 5)
5.times do
spawn do
while (order_id = raw_orders.receive?)
result = process_single_order(order_id) rescue nil
processed.send(JobResult.new(order_id, success: !result.nil?))
end
end
end
# Consumer: aggregate results
spawn do
count = 0
while (result = processed.receive?)
count += 1
puts "Processed #{count}: #{result.order_id} (#{result.success ? "OK" : result.error})"
end
end
# Wait for all fibers
Fiber.yield
end
HTTP Server
# src/server.cr — HTTP server with routing
require "http/server"
require "json"
class OrdersHandler
include HTTP::Handler
def call(context : HTTP::Server::Context)
request = context.request
response = context.response
response.content_type = "application/json"
case {request.method, request.path}
when {"GET", /^\/orders\/(?<id>[a-z0-9-]+)$/}
order_id = $~["id"]
handle_get_order(order_id, response)
when {"POST", "/orders"}
handle_create_order(request, response)
when {"GET", "/orders"}
handle_list_orders(request, response)
else
call_next(context)
end
end
private def handle_get_order(id : String, response : HTTP::Server::Response)
order = Order.find(id)
if order
response.print order.to_json
else
response.status = HTTP::Status::NOT_FOUND
response.print({error: "Order not found"}.to_json)
end
end
private def handle_create_order(request : HTTP::Request, response : HTTP::Server::Response)
body = request.body.try(&.gets_to_end) || ""
begin
params = JSON.parse(body)
order = Order.create(
customer_id: params["customer_id"].as_s,
items: parse_items(params["items"]),
)
response.status = HTTP::Status::CREATED
response.print order.to_json
rescue ex : JSON::ParseException
response.status = HTTP::Status::BAD_REQUEST
response.print({error: "Invalid JSON: #{ex.message}"}.to_json)
rescue ex : ArgumentError
response.status = HTTP::Status::UNPROCESSABLE_ENTITY
response.print({error: ex.message}.to_json)
end
end
private def handle_list_orders(request : HTTP::Request, response : HTTP::Server::Response)
status_param = request.query_params["status"]?
status = status_param ? OrderStatus.parse(status_param) : nil
orders = Order.list(status: status, limit: 50)
response.print orders.to_json
end
private def parse_items(json : JSON::Any) : Array(OrderItem)
json.as_a.map do |item|
OrderItem.new(
product_id: item["product_id"].as_s,
product_name: item["product_name"].as_s,
quantity: item["quantity"].as_i,
unit_price: Money.new(item["unit_price_cents"].as_i64),
)
end
end
end
# Main server entry point
server = HTTP::Server.new([
HTTP::LogHandler.new,
HTTP::ErrorHandler.new,
OrdersHandler.new,
])
Signal::INT.trap { server.close }
address = server.bind_tcp("0.0.0.0", 8080)
puts "Listening on http://#{address}"
server.listen
C Bindings
# src/bindings/openssl.cr — C library bindings
@[Link("ssl")]
@[Link("crypto")]
lib LibSSL
fun sha256 = SHA256(
data : UInt8*,
len : LibC::SizeT,
md : UInt8*
) : UInt8*
end
def sha256(data : Bytes) : Bytes
digest = Bytes.new(32)
LibSSL.sha256(data, data.size, digest)
digest
end
def sha256_hex(data : String) : String
sha256(data.to_slice).hexstring
end
Spec Tests
# spec/order_spec.cr — RSpec-like testing
require "spec"
require "../src/models/order"
describe Order do
describe "#high_value?" do
it "returns false for orders under $100" do
items = [OrderItem.new("p1", "Widget", 1, Money.new(5_00))]
order = Order.new("ord-1", "cust-1", items)
order.high_value?.should be_false
end
it "returns true for orders over $100" do
items = [OrderItem.new("p1", "Widget", 2, Money.new(75_00))]
order = Order.new("ord-1", "cust-1", items)
order.high_value?.should be_true
end
end
describe "#total" do
it "sums all item subtotals" do
items = [
OrderItem.new("p1", "Item A", 2, Money.new(10_00)),
OrderItem.new("p2", "Item B", 1, Money.new(25_00)),
]
order = Order.new("ord-1", "cust-1", items)
order.total.cents.should eq(45_00)
end
end
describe "Money" do
it "adds same-currency amounts" do
a = Money.new(10_00)
b = Money.new(25_50)
(a + b).cents.should eq(35_50)
end
it "raises on currency mismatch" do
expect_raises(ArgumentError, "Currency mismatch") do
Money.new(100, "USD") + Money.new(100, "EUR")
end
end
end
end
shards.yml
name: order-service
version: 0.1.0
dependencies:
pg:
github: will/crystal-pg
version: ~> 0.28
redis:
github: stefanwille/crystal-redis
version: ~> 2.9
targets:
server:
main: src/server.cr
For the Zig systems language that also compiles to native code but emphasizes explicit memory management over Crystal’s GC, see the Zig guide for comptime generics and C interop. For the Ruby web framework that Crystal’s syntax derives from, where you need ecosystem breadth over raw performance, the Rails guide covers the full Rails stack. The Claude Skills 360 bundle includes Crystal skill sets covering fiber concurrency, C bindings, and HTTP service development. Start with the free tier to try Crystal program generation.