Natra
Natra is a command line generator for small Sinatra services. natra new creates a JSON API skeleton with ActiveRecord, PostgreSQL, Puma, RSpec, RuboCop, a /health endpoint and a Docker setup. Inside that app, natra model, natra controller, natra scaffold and natra service_object add models with migrations, JSON CRUD controllers with request specs, and plain Ruby service objects. HTML views are available with --views.
Requirements
- Ruby 3.0 or newer to run natra and the apps it generates. Apps generated on Ruby 3.2 or newer use ActiveRecord 8.1; on Ruby 3.0 and 3.1 they use ActiveRecord 7.1, pg 1.6 and redis 5.4, the newest releases that support those Rubies.
- PostgreSQL for the generated app's database.
- Docker with Compose v2, only if you want the generated Docker setup.
natra newrunsdocker compose build --pullas its last step. Without Docker that step prints an error, but the app is still generated and you can run it locally.
Installation
gem install natraQuick start
natra new my-api
cd my-apiRun with Docker
secrets.env holds the database settings for the web and db containers.
docker compose run --rm web bin/setup
docker compose upRun locally
Start PostgreSQL first. Without DATABASE_URL, config/database.yml uses the databases development_my_api and test_my_api (override them with DEV_DATABASE and TEST_DATABASE).
bundle install
bundle exec rake db:create db:migrate
bundle exec puma -C config/puma.rbEither way the app listens on http://localhost:9292. Run its specs with bundle exec rspec after RACK_ENV=test bundle exec rake db:create db:migrate.
Add a resource
natra scaffold post title:string body:text
bundle exec rake db:migrateRestart Puma, then:
$ curl -s -X POST localhost:9292/posts -H 'Content-Type: application/json' \
-d '{"title":"Hello","body":"First post"}'
{"id":"a43585d4-9b1e-4bb5-96b3-966edc9566e0","title":"Hello","body":"First post","created_at":"2026-10-04T19:26:10.670Z","updated_at":"2026-10-04T19:26:10.670Z"}
$ curl -s localhost:9292/posts
[{"id":"a43585d4-9b1e-4bb5-96b3-966edc9566e0","title":"Hello","body":"First post",...}]
$ curl -s -X PATCH localhost:9292/posts/a43585d4-9b1e-4bb5-96b3-966edc9566e0 \
-H 'Content-Type: application/json' -d '{"title":"Hello again"}'
{"id":"a43585d4-9b1e-4bb5-96b3-966edc9566e0","title":"Hello again",...}
$ curl -s -o /dev/null -w '%{http_code}\n' -X DELETE localhost:9292/posts/a43585d4-9b1e-4bb5-96b3-966edc9566e0
204The generated API
Every response is JSON. The generated ApplicationController sets the content type, provides the json(object, status = 200) and json_params helpers, and turns errors into JSON responses:
| Status | When | Body |
|---|---|---|
| 400 | The request body is not valid JSON, or not a JSON object | {"error":"Invalid JSON"} |
| 404 | Unknown route, or ActiveRecord::RecordNotFound
|
{"error":"Not found"} |
| 422 |
ActiveRecord::RecordInvalid, for example a failed validates
|
{"errors":{"title":["can't be blank"]}} |
| 500 | Any other error |
{"error":"Internal server error"}, plus the exception message in development only. The backtrace goes to the log, never to the client. |
natra scaffold post title:string body:text generates these routes:
| Route | Success | Errors |
|---|---|---|
GET /posts |
200, a JSON array ordered by created_at
|
|
GET /posts/:id |
200, the record | 404 |
POST /posts |
201, the created record | 400, 422 |
PATCH /posts/:id |
200, the updated record | 400, 404, 422 |
DELETE /posts/:id |
204, no body | 404 |
Create and update read the JSON request body and only accept the scaffold's fields (title and body here), so other keys such as id are ignored. natra controller NAME generates the same routes when app/models/NAME.rb exists, accepting the fields you pass (natra controller post title body) or, without fields, the model's columns other than id, created_at and updated_at. Without a model it generates a stub controller with a GET index and show and a TODO; generate the model, then run natra controller NAME again and let it overwrite the stub.
Health check
GET /health runs SELECT 1 against the database:
$ curl -s localhost:9292/health
{"status":"ok","database":"ok"}If the database cannot be reached it returns 503 with {"status":"error","database":"unavailable"}. The Dockerfile has a HEALTHCHECK that calls it with curl, and docker-compose.yml health-checks both containers and starts web only once PostgreSQL is healthy. GET / returns the app name, such as {"name":"MyApi","status":"ok"}.
Request specs
natra new writes spec/requests/application_spec.rb, which covers /, /health (including the 503) and unknown routes. natra scaffold writes spec/requests/posts_spec.rb, which checks every route above with its success and error statuses, using sample values for the fields. The specs use rack-test against config.ru, and spec/spec_helper.rb wraps each example in a transaction with database_cleaner-active_record and provides json_body and json_request(method, path, payload).
HTML views
The default output is JSON only. To render HTML with erb instead:
-
natra new APP_PATH --viewsaddsapp/views/layout.erb,app/views/welcome.erbandpublic/favicon.ico, and serves the welcome page atGET /. -
natra controller NAME --viewsandnatra scaffold NAME --viewsgenerate HTML routes (index, new, create, show, edit, update and delete, with placeholder views and redirects) instead of the JSON controller, and no request spec.
Commands
| Command | What it does | Options |
|---|---|---|
natra new APP_PATH |
Creates a new Sinatra application in APP_PATH
|
--views adds an HTML layout, welcome page and public/--git runs git init and git add .--bundle runs bundle install--redis adds the redis gem, config/redis.yml and a Redis initializer--capistrano runs cap install--rvm writes .ruby-version (the Ruby running natra) and .ruby-gemset, and skips --bundle
|
natra model NAME [field:type ...] |
Generates a model and a migration that creates its table |
--no-migration skips the migration |
natra controller NAME [field ...] |
Generates a JSON controller and mounts it in config.ru: CRUD routes if the model exists, otherwise a stub |
--views generates HTML routes and erb views instead |
natra scaffold NAME [field:type ...] |
Runs model and controller for NAME and writes a request spec |
--views generates HTML routes and erb views instead, without a request spec--no-migration skips the migration |
natra service_object NAME |
Generates a service object in app/services
|
|
natra -v, natra --version
|
Prints the natra version | |
natra help [COMMAND] |
Lists the commands, or describes one |
Run model, controller, scaffold and service_object from the root of a generated app.
Naming rules:
-
APP_PATHis lowercased, and characters other than letters,-and_are dropped.natra new My-Blogcreatesmy-blog. - Fields are written
name:type, for exampletitle:string body:text published:boolean. The type defaults tostring, sotitleis the same astitle:string. - Models are singular (a plural name is singularized with a warning). Controllers and service objects are pluralized:
natra controller postcreatesPostsController, andnatra service_object paymentcreatesPaymentsService.
What you get
natra new blog creates:
blog/
├── .gitignore
├── .rspec
├── .rubocop.yml
├── Dockerfile
├── Gemfile
├── Guardfile
├── README.md
├── Rakefile
├── config.ru
├── docker-compose.yml
├── secrets.env
├── app/
│ ├── controllers/application_controller.rb
│ ├── models/
│ └── services/
├── bin/setup
├── config/
│ ├── database.yml
│ ├── environment.rb
│ ├── puma.rb
│ └── initializers/oj.rb
├── db/
│ ├── migrate/YYYYMMDD0000_add_extensions.rb
│ └── seeds.rb
├── lib/
└── spec/
├── requests/application_spec.rb
├── spec_helper.rb
└── support/
The Gemfile uses Sinatra 4, ActiveRecord (8.1, or 7.1 on Ruby 3.0 and 3.1) through sinatra-activerecord, pg, Puma, Oj, rack-timeout and Scout APM, with RSpec, rack-test, FactoryBot, Faker, DatabaseCleaner, SimpleCov and Guard for tests. The first migration enables the hstore, uuid-ossp and pgcrypto PostgreSQL extensions. The Rakefile loads the sinatra-activerecord tasks, such as db:create, db:migrate, db:seed and db:create_migration. rake db:seed loads db/seeds.rb, which is plain Ruby, and bin/setup runs it after migrating.
natra scaffold post title:string body:text then creates:
app/models/post.rb # class Post < ActiveRecord::Base
db/migrate/YYYYMMDDHHMMSS_create_posts.rb # posts table with a UUID id, title, body and timestamps
app/controllers/posts_controller.rb # JSON index, show, create, update and delete routes
spec/requests/posts_spec.rb # request specs for every route and status
It also adds use PostsController to config.ru.
Development
Natra is developed on Ruby 3.3.10 (see .ruby-version). CI runs on Ruby 3.0, 3.1, 3.2, 3.3, 3.4.3, 3.4.8, 3.4.9 and the latest 3.4.
bin/setup # install dependencies
bundle exec rspec # run the specs
bundle exec rubocop # lint
bin/console # Pry session with natra loadedSimpleCov measures line and branch coverage and writes a report to coverage/. CI requires 100% line and branch coverage. Run CI=true bundle exec rspec to apply the same check locally.
To release a new version:
- Update the version number in
lib/natra/version.rb. - Move the entries under "Unreleased" in CHANGELOG.md to a section for the new version.
- Commit, then run
bundle exec rake release. This builds the gem, tags the version, pushes the commit and tag, and pushes the gem to rubygems.org.
Changelog
Notable changes are listed in CHANGELOG.md.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/jamesnjuguna0419/natra.
- Open an issue first for larger changes, so the approach can be agreed before you write code.
- Fork the repository and create a branch from
master. - Make your change and add or update specs for it. If you change what a generator produces, update its spec under
spec/natra/generators/. - Check that
CI=true bundle exec rspecpasses with 100% line and branch coverage, and thatbundle exec rubocopreports no offenses. - Add a line to the "Unreleased" section of CHANGELOG.md.
- Open a pull request that explains what changed and why.
Code of Conduct
This project is intended to be a safe, welcoming space for collaboration. Everyone interacting in the Natra project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the Code of Conduct, which is adapted from the Contributor Covenant.
License
The gem is available as open source under the terms of the MIT License.