Your first app#
This page takes you from an empty folder to a running Spider-Gazelle API. Start here if you're new to Spider-Gazelle, or new to Crystal.
Spider-Gazelle doesn't depend on anything beyond Crystal and its package manager, Shards. There are two ways to start:
- From the application template (recommended). You get a project with logging, sessions, error handling, specs, a Dockerfile, CI, OpenAPI docs and an MCP server already wired up.
- From scratch. You add the
action-controllershard to a new Crystal project. This is useful for learning, or for adding an HTTP API to an existing project.
Install Crystal#
Follow the Crystal installation guide for your operating system, then check it worked:
crystal --version
shards --version
Start from the template#
The spider-gazelle template is a complete, working application. Clone it into a folder named after your project, and give it a fresh git history:
git clone https://github.com/spider-gazelle/spider-gazelle.git my_app
cd my_app
rm -rf .git && git init
shards install
crystal run src/app.cr
The app compiles and starts listening:
Launching Spider-Gazelle v2.0.0
Listening on http://127.0.0.1:3000
Open http://localhost:3000 and you'll see "You're being trampled by Spider-Gazelle!".
Try http://localhost:3000/api/42 too, which returns {"result":42}.
Note
Crystal is a compiled language, so crystal run compiles your app before it
starts. The first build takes a little longer, later builds are cached.
Project layout#
| Path | Purpose |
|---|---|
src/app.cr |
Entry point. Parses the command line options and starts the server. |
src/config.cr |
Requires your code and configures logging, middleware, sessions and the MCP server. |
src/constants.cr |
App name, version and settings read from environment variables. |
src/controllers/application.cr |
App::Base, the abstract controller every other controller inherits from. Holds shared filters, responders and error handlers. |
src/controllers/welcome.cr |
An example controller. |
src/models/ |
Your models. |
spec/ |
Your specs. |
www/ |
Static files, served when no route matches. |
Dockerfile |
Builds a small production image, see Deployment. |
.github/workflows/ci.yml |
Checks formatting and runs the specs on every push. |
Change the app name in src/constants.cr (NAME = "Spider-Gazelle") and the
name in shard.yml. Before you deploy, set your own session secret, see
Configuration.
Start from scratch#
Create a new Crystal app and add action-controller to its shard.yml:
crystal init app my_app
cd my_app
dependencies:
action-controller:
github: spider-gazelle/action-controller
version: ~> 9.2
Run shards install, then replace src/my_app.cr with:
require "action-controller"
# Says hello
class Hello < AC::Base
base "/"
# Returns a friendly greeting
@[AC::Route::GET("/")]
def index : String
"Hello, World!"
end
end
# The server must be required after your controllers
require "action-controller/server"
server = AC::Server.new(3000, "127.0.0.1")
server.run { puts "Listening on #{server.print_addresses}" }
Start it with crystal run src/my_app.cr and request the route:
$ curl http://localhost:3000/
"Hello, World!"
The response is "Hello, World!" with quotes because responses are JSON by default.
The client's Accept header selects the format, see
Responses.
AC is an alias for ActionController, so AC::Base and
ActionController::Base are the same class.
Your first route#
A controller is a class that inherits from AC::Base, and a route is a method
with a route annotation. Add a second route to the Hello controller:
# Greets someone by name, e.g. GET /greet/Steve?shout=true
@[AC::Route::GET("/greet/:name")]
def greet(name : String, shout : Bool = false) : String
greeting = "Hello, #{name}!"
shout ? greeting.upcase : greeting
end
$ curl "http://localhost:3000/greet/Steve?shout=true"
"HELLO, STEVE!"
There's no params["name"] boilerplate here:
namematches the:namesegment in the route path.shoutisn't in the path, so it's read from the query string. It has a default, so it's optional. ABoolistruewhen the value istrue(in any case) andfalseotherwise.- Values are converted to the argument type. If a value can't be converted, for
example
abcfor anInt32argument, Spider-Gazelle responds with an error instead of calling your method.
That one annotated method is also your documentation. The doc comment, argument types and return type become the OpenAPI operation and the MCP tool description, so your API docs and LLM tools can't drift from the code.
From here, read the guides:
- Routing: controllers, base paths, HTTP verbs and the route DSL.
- Parameters: type conversion,
@[AC::Param::Info]and request bodies. - Responses: response formats, status codes and headers.
- Filters: run code before, around or after your routes.
- Errors: turn exceptions into consistent error responses.
Run the specs#
The template ships with specs that use Crystal's built-in spec library:
crystal spec
See Testing to write your own.
Reload while you develop#
Crystal doesn't reload code at runtime. To rebuild and restart whenever a file changes, use a file watcher such as watchexec or nodemon:
watchexec -r -e cr -- crystal run src/app.cr
# or
nodemon --watch src -e cr --exec crystal run src/app.cr
Build a binary#
For production, compile an optimised binary. With the template, shards build
uses the app target in shard.yml and writes bin/app:
shards build --production --release
./bin/app --help
The binary has options for the port, host, worker count, routes listing, health checks and generating the OpenAPI and MCP description files. See Configuration. For containers, see Deployment.
See also#
- Configuration: environment variables,
config.crand command line options - Routing
- Testing
- Deployment
- OpenAPI and MCP