Guide 11 of 12

From Rails

A dictionary for the Rails developer reading or extending a Bogie app: what each Rails thing is called here, where it lives, and what has no equivalent. When Rails has a word for something, Bogie uses it; this guide is for the places the word is the same but the thing underneath is not, and for the places there is no word because there is no thing.

The one idea to carry over: Go has no autoload and no method_missing. Nothing exists until it is written down and registered, so every "magic" Rails step has a visible line here, and the generators write those lines so you do not have to find them.

Commands

RailsBogieNotes
rails new blogbogie new blog--jobs adds River. API-only, Postgres-only; no --api, no -d.
rails server, rails sbogie server, bogie sMigrates at boot, as a deployed container does. Reloads on save in development.
bin/dev, Springnone; built into bogie server and bogie workerSave a file and Air rebuilds and restarts it; a new migration is picked up on the next boot too. Production runs the compiled binary, no watcher.
rails g scaffold post title:stringbogie g scaffold post title:stringModel, views and a wired controller; no HTML.
rails g model post title:stringbogie g model post title:stringMigration, queries, domain type, store; runs sqlc.
rails g controller posts index showbogie g controller posts index showWrites it and registers it. new and edit are refused.
rails g controller admin/reportsbogie g controller admin/reportsA namespace is a package, app/controllers/admin_controller/.
rails g migration add_slug_to_posts slug:stringbogie g migration add_slug_to_posts slug:stringSQL, not Ruby; create_, add_x_to_, remove_x_from_ get their SQL written.
rails g job send_welcomebogie g job send_welcomeA River worker, registered; the first one on an app with no jobs adds them first.
rails g authenticationbogie g authentication secret, token or api_keyA middleware on a route group; the shape says who the caller is. See Authentication.
(a service object, by hand)bogie g service publish_postA package with a Run(ctx), not wired anywhere.
rails destroy ..., rails dbogie destroy ..., bogie dRemoves the registration lines too.
rails db:create … db:rollbackthe same wordsdb:prepare, db:reset, db:seed, db:migrate:status, db:migrate:redo, db:version.
rails credentials:editbogie credentials:edit-e production for that environment's file and key.
rails testbogie test, bogie tgo test -race -cover ./...; store tests need Postgres up.
bin/cibin/ciThe whole pipeline, locally; green signs off with gh signoff.
rails routesnoneRead app/controllers/routes.go and each SetupRoutes; it is one screen.
rails consolenoneNo REPL. make psql opens the database; a throwaway _test.go is the Go way.
rails dbconsolemake psql
bundle installgo mod downloadgo mod tidy is bundle after a Gemfile edit.
rubocopbogie lintgofmt, go vet, golangci-lint; .golangci.yml is the config.
kamal deploykamal deploySame tool, same config/deploy.yml.

There is also a bogie doctor with no Rails equivalent: it checks that the markers the generators insert above are intact and that every controller is both constructed and mounted, which Rails never needed to check because it autoloads.

Where things live

RailsBogieNotes
config/routes.rbapp/controllers/routes.goThe table; each controller's SetupRoutes adds its own routes under a group.
app/controllers/posts_controller.rbapp/controllers/posts_controller.goA PostsController type: its dependencies, routes and actions.
app/controllers/admin/reports_controller.rbapp/controllers/admin_controller/reports_controller.goThe package holds a Server with one field per controller and a routes.go for the group's middleware.
app/models/post.rbapp/domain/post.go + app/models/posts.go + db/queries/posts.sqlSplit in three: the type and its validations; the store methods; the SQL. See below.
app/views/posts/*.json.jbuilderapp/views/posts.goRequest and response structs. A domain type never doubles as a wire type.
app/jobs/send_welcome_job.rbapp/jobs/send_welcome.goAn Args struct and a Worker, registered in app/jobs/jobs.go.
app/services/app/services/<name>/One package per service, ctx first, knows nothing about HTTP.
app/helpers/, lib/utils.rbnoneRule 6: no util, helpers or common. A package is named for what it provides.
app/controllers/concerns/noneCompose with plain functions and small interfaces.
db/migrate/*.rbdb/migrate/*.sqlgoose SQL with -- +goose Up and Down, embedded in the binary.
db/schema.rbnoneThe migrations are the schema; sqlc compiles the queries against them.
db/seeds.rbdb/seeds/*.sqlVersioned like migrations, in their own table.
config/database.ymlDATABASE_URL in credentials or the environmentOne URL per environment; test never reads a credentials file.
config/credentials.yml.enc, master.keythe same filesSame pattern, same names, same -e production.
config/application.rb, initializersapp/application.goWhere everything is constructed and wired, in order, with the markers.
config/puma.rbnonenet/http with timeouts set in app/controllers/application.go.
config/deploy.ymlconfig/deploy.ymlKamal 2, unchanged.
Gemfile, Gemfile.lockgo.mod, go.sumTools (sqlc, goose) are pinned there too, under tool.
.rubocop.yml.golangci.yml
test/, spec/beside the code, *_test.goStdlib testing, no factories, no fixtures.
Procfile.dev, bin/devmake server, make worker
config/spring.rb.air.tomlSettings for the reload bogie server and bogie worker run under in development; delete it to turn reload off.
AGENTS.md, CLAUDE.mdthe sameThe rules, the commands, a recipe per task.

A model, in three places

Rails's Post is one class that is a table, a type and a query builder. Here that is three files, each with one job:

So Post.find(id) is store.GetPost(ctx, id), Post.create(attrs) is store.CreatePost(ctx, domain.Post{...}), and a new query is a new named statement in the .sql file plus a store method that calls it. There is no where builder; the SQL is written out, which is the point.

Associations are a foreign key and a query. post:references on a model writes the column, the constraint and the index; has_many :comments is ListCommentsForPost in db/queries/comments.sql when you need it.

In the code

RailsBogie
params[:id]c.Param("id")
params.require(:post).permit(:title)var req views.PostRequest; c.ShouldBindJSON(&req); only the struct's fields come through
render json: postc.JSON(http.StatusOK, postView(post))
head :no_contentc.Status(http.StatusNoContent)
validates :title, presence: truefunc (p Post) Validate() error { ... return fmt.Errorf("%w: title is required", ErrInvalid) } in app/domain; runs on save
rescue_from ActiveRecord::RecordNotFoundctl.fail(c, err): ErrNotFound is 404, ErrInvalid is 422, anything else is a logged 500
before_action :authenticatemiddleware on the group, in routes.go, so the exposure boundary is visible in one place; bogie g authentication writes it and a WithUser group helper. See Authentication.
Current.usermiddlewares.Subject(c), the verified token's subject; load the record if you need it
Post.find(id)ctl.Store.GetPost(ctx, id)
post.savestore.CreatePost(ctx, post) or store.UpdatePost(ctx, post); validation runs inside
SendWelcomeJob.perform_later(id)app.Jobs.Insert(ctx, jobs.SendWelcomeArgs{ID: id}, nil); InsertTx inside a transaction
Rails.logger.infolog.InfoContext(ctx, ...), a *slog.Logger passed in, never global
Rails.env.production?cfg.Env == config.EnvProduction
ENV["X"], Rails.application.credentials.xa field on the config.Config struct, validated at boot
request.request_idc.GetString("request_id"), set by the middleware
Rails.cachenone
ActiveSupport::Notificationsthe request log line; OpenTelemetry rides in ctx when you add it

What is deliberately different

These have no Rails counterpart because Rails does them implicitly. In Go they are explicit, and the six rules in Conventions and agents keep them that way.

  1. Dependencies are passed in, in app/application.go. Nothing reaches for a global. A controller gets its store on its wire line; a test hands it a fake.
  2. Interfaces are declared by the consumer (rule 2), not by the thing implemented. postsStore lives beside PostsController and lists only the five methods it calls. That is why a controller test needs no database, and why there is no mocking library.
  3. ctx context.Context is the first parameter of anything that does I/O (rule 1). Cancellation, deadlines and tracing travel in it; a request that is abandoned stops its query.
  4. Constructors return (T, error) (rule 4); only main exits. A bad configuration fails at boot with a message, never with a panic in a handler.
  5. *gin.Context stops at app/controllers (rule 5). A handler converts HTTP into domain values, calls one function, and converts the result back. Below that, the code can be called from a job, a CLI or a test.
  6. Registration is written down. A controller exists once it is held, constructed and mounted, three lines above three // bogie: markers. The generators write them; bogie doctor checks them.

What has no equivalent

Honest list. Some are API-only by design; some are Rails features Go does not need; some are gaps.