rails new --api is a decision about the next three years taken in the first ten seconds of a
project, which is a bad moment to take it. The flag is easy to read as "the JSON one", and what it
actually does is remove a specific list of things, some of which are cheap to add back and some of
which you will add back badly.
Everything below is Rails 8.1.3.1, read out of railties-8.1.3.1 and actionpack-8.1.3.1 rather
than recalled.
The flag writes one line, and the rest follows from it
rails new --api puts config.api_only = true into your generated config/application.rb. The
template that writes it, lib/rails/generators/rails/app/templates/config/application.rb.tt, sits
the line under a three-line comment: "Only loads a smaller set of middleware suitable for API only
apps. Middleware like session, flash, cookies can be added back manually. Skip views, helpers and
assets when generating a new resource."
The setter does more than store the value. In Rails::Application::Configuration:
So one assignment reaches three subsystems: the middleware stack reads config.api_only while it
builds, the generators get retuned through Rails::Generators.api_only!, and the debug exception
format flips to :api, which is why a development error in an api_only app comes back as text
rather than as the familiar HTML error page with the source extract.
The flag is also not a meta option, and the Rails source says so in a comment on
Rails::Generators::AppBase#imply_options: "In contrast, --api is not a meta option because it does
other things besides implying options such as --skip-asset-pipeline." What it does imply is
--skip-asset-pipeline and --skip-javascript, and --skip-javascript in turn implies
--skip-hotwire.
The middleware an api_only stack never builds
Rails::Application::DefaultMiddlewareStack#build_stack is a single method with seven
unless config.api_only guards in it, and reading them is the honest version of what the flag
removes:
Seven entries, and the interesting thing is which ones are not on the list. ActionDispatch::Static
still runs, so an api_only app still serves whatever is in public/. Rack::ETag and
Rack::ConditionalGet still run, so conditional GET works on JSON exactly as it does on HTML.
ActionDispatch::RemoteIp, ActionDispatch::RequestId, ActionDispatch::Executor and
Rails::Rack::Logger are all untouched. The stack is thinner by seven, not by half.
Rack::MethodOverride is the one people forget they lost. It is the middleware that turns a form
POST carrying _method=DELETE into a DELETE, and an API served to a JavaScript client that sends
real verbs never notices it is gone. The day somebody adds one HTML form to the same application,
form_with method: :delete posts, and the router answers with a routing error for POST on a path
that only accepts DELETE.
The controller modules ActionController::API leaves out
ActionController::API and ActionController::Base both declare a MODULES constant, and the
difference between the two arrays is the whole story. Base includes 36 modules; API includes 17.
What API has that Base does not: ApiRendering and BasicImplicitRender, two modules.
What Base has that API does not, all 21 of them: AbstractController::Translation,
AbstractController::AssetPaths, Helpers, ActionView::Layouts, Rendering,
EtagWithTemplateDigest, EtagWithFlash, MimeResponds, ImplicitRender, ParameterEncoding,
Cookies, Flash, FormBuilder, RequestForgeryProtection, ContentSecurityPolicy,
PermissionsPolicy, AllowBrowser, Streaming, and the three HttpAuthentication controller
method modules for Basic, Digest and Token.
Three of those are worth naming individually because they are the ones that surprise people.
AbstractController::Translation is what defines the t and l shortcuts in a controller, so
t("api.errors.invalid_credentials") raises NoMethodError in an ActionController::API
subclass and you write I18n.t in full. MimeResponds is what defines respond_to do |format|,
which is the one Rails names in its own documentation as the thing you are most likely to include
back. And HttpAuthentication::Token::ControllerMethods is what gives you
authenticate_with_http_token, so an API controller that wants a bearer token parses the
Authorization header itself.
The class documentation states the boundary plainly: an API controller "doesn't include a number of
features that are usually required by browser access only: layouts and templates rendering, flash,
assets, and so on", and "Request, response, and parameters objects all work the exact same way as
ActionController::Base."
The 204 that means somebody forgot to render
ActionController::Base includes ImplicitRender, which looks for a template matching the action
and raises ActionController::MissingExactTemplate when there is none. ActionController::API
includes BasicImplicitRender instead, and the quoted block below is the whole of it:
An action that falls off the end without rendering answers 204 No Content. Rails documents it as
a rule to follow rather than as a hazard: "you need to ensure your controller is calling either
render or redirect_to in all actions, otherwise it will return 204 No Content."
Consider what that means on a guard clause. A before_action that returns early without rendering,
an if branch with no else, a rescue that logs and swallows: each one produces a 204, and 204
is a success. response.ok? is false, response.successful? is true, fetch resolves, and most
client libraries report it as a win with an empty body. Under ActionController::Base the same bug
is a 500 with an exception name in it. This is the trade the thinner stack makes: it cannot tell a
deliberate empty response from a missing one, because it has no template to look for.
Generators, and the files rails new --api deletes
Rails::Generators.api_only! is what config.api_only = true triggers, and the whole method is this:
So rails generate scaffold Post in an api_only app produces a controller and a model and no views,
no helper and no stylesheet. The mailer keeps ERB explicitly, on the last two lines, because an API
that sends no email is not what anybody meant.
The generator also deletes, once, at creation time. AppGenerator removes app/assets and
app/helpers entirely, removes app/views/layouts/application.html.erb and app/views/pwa,
removes config/initializers/content_security_policy.rb, and creates
config/initializers/cors.rb with every line commented out and gem "rack-cors" commented out in
the Gemfile to match. app/views itself survives as long as Action Mailer does, which is how the
mailer templates keep a home.
Then it removes public/400.html, public/404.html, public/406-unsupported-browser.html,
public/422.html, public/500.html, public/icon.png and public/icon.svg. That last deletion
has a consequence nobody mentions, and the next section is it.
A 500 that a browser reads as an empty 404
ActionDispatch::PublicExceptions is the middleware that turns an unhandled exception into a
response in production, and it branches on the requested format. For JSON it builds
{ status: 500, error: "Internal Server Error" } and serialises it. For HTML it goes looking for a
file:
In an api_only application public/500.html was deleted at generation time, so that else branch
is the one that runs. A browser pointed at a broken endpoint of a Rails API only application gets
404 with an empty body and an X-Cascade: pass header, for a request that failed with a 500.
The reason this survives so long in a real project is that nothing in a normal test run reaches it.
Request specs assert on JSON, config.consider_all_requests_local is true in development so
DebugExceptions handles the error long before PublicExceptions sees it, and the format branch
only misbehaves for text/html. The suite stays green. What finds it is an uptime monitor, or a
support ticket from somebody who pasted an API URL into a browser bar, and both of them report
"your API returns 404" for an endpoint that is actually crashing.
Writing to the session raises; reading it returns nil
session is available on every controller regardless of stack, because
ActionController::Metal carries delegate :session, to: "@_request". What differs in an api_only
application is that no session store was ever inserted into the middleware, so the
ActionDispatch::Request::Session object is there and disabled.
The two halves behave differently. load_for_write! raises
ActionDispatch::Request::Session::DisabledSessionError with the message "Your application has
sessions disabled. To write to the session you must first configure a session store". load_for_read!
is load! if !loaded? && exists?, and with sessions disabled exists? is false, so nothing loads,
the backing hash stays empty, and session[:user_id] returns nil.
A loud failure on write and a silent nil on read is the worst pairing for the code people actually
write. session[:user_id] = user.id in a sign-in path blows up in the first minute and gets fixed.
if session[:return_to] in a redirect path is just a branch that never fires, forever, and it looks
exactly like a branch whose condition happens to be false.
What it costs to put cookies and sessions back
Rails is explicit that the removals are reversible, and the guide names Rack::MethodOverride,
ActionDispatch::Cookies and ActionDispatch::Flash as things you can re-insert. The mechanical
part is two lines in config/application.rb:
Order matters, since the session store reads the cookie jar the previous middleware built, and
ActionDispatch::Flash will need both of them under it. The controller side needs
include ActionController::Cookies for the cookies helper, and include ActionController::Flash
if you want flash.
The part that does not come back is the part you needed. RequestForgeryProtection is a controller
module, not middleware, so re-inserting ActionDispatch::Cookies and a cookie session store gives
you a browser-style session with no CSRF defence attached to it. Nothing warns. protect_from_forgery
is not defined on your controllers until you include the module, and until then every cookie-authenticated
endpoint accepts a cross-site POST. An api_only application that grew a login form is the exact
shape where that happens, because the login form is the thing that made somebody add the session
back in the first place.
The honest accounting: three lines of middleware, two includes, one protect_from_forgery call,
and a security property you have to remember to restore rather than one you inherit. None of that is
hard. All of it is easy to get 80 percent of.
Why this boilerplate runs ActionController::API inside a full Rails app
The JSON API of this boilerplate is not an api_only application.
No file in its config/ sets config.api_only, its
config/application.rb requires action_view/railtie and action_cable/engine along with the
rest, and it ships 48 controller classes against 30 directories of views. The JSON API is three of those
controllers, under app/controllers/api/v1/.
The base controller reaches for ActionController::API directly:
Inheriting from ActionController::API rather than from ApplicationController is the load-bearing
line. ApplicationController in this codebase carries session authentication, the onboarding gate,
CSRF protection and a layout, and every one of those is wrong for a bearer-token endpoint. Skipping
it means the API controllers never inherit a browser callback they then have to remember to
skip_before_action, which is the failure mode of the other arrangement: a skip_before_action
list that drifts one line behind ApplicationController and lets a redirect-to-sign-in leak into a
JSON response.
What the full application buys is that there is one of it. The marketing site, the admin console at
/admin, the Stripe checkout and /api/v1/user are one router, one deploy, one set of credentials
and one User model. The api_only version of the same product is two applications sharing a
database, or one application plus a separate front end, and either way the admin screen you need in
week three is a second thing to deploy. For a product sold to one developer who wants to ship this
weekend, one deployment wins, and the cost of that position is in the next section.
The token half of this arrangement, Api::Auth::IssueToken, User.authenticate_by and the
Authorization header parsing that stands in for the missing
authenticate_with_http_token, is worked through in
Rails JWT API authentication.
The gated_by :api line on the first page of that controller belongs to a different mechanism, and
a disabled feature answering 404
explains why the word prepend in it is what keeps a switched-off API from redirecting anonymous
callers to a sign-in page.
What the full middleware stack costs the JSON endpoints
Every request to /api/v1/user in this codebase passes through ActionDispatch::Cookies,
ActionDispatch::Session::CookieStore, ActionDispatch::Flash,
ActionDispatch::ContentSecurityPolicy::Middleware, Rack::MethodOverride and
Rack::TempfileReaper, because the application is not api_only and those are in the stack for the
HTML half. bin/rails middleware prints all of them.
The cost is real and it is small. The session store does no work when nothing reads or writes the
session, and the API controllers never do: a cookie jar that is never touched is parsed lazily and
the response carries no Set-Cookie. Rack::MethodOverride declares ALLOWED_METHODS = %w[POST], so it
reads nothing at all on the GET and PATCH this API serves. The flash loads from a session that was
never loaded. Measured as throughput on a
JSON endpoint, six middleware that mostly return immediately is not the thing that decides whether
your API is fast.
The cost that is not small is conceptual, and it is worth stating rather than waving away. Cookies
being present means a future contributor can write session[:something] in an API controller and it
will work, in development and in tests, and the endpoint stops being stateless without anybody
deciding that it should. An api_only application gets DisabledSessionError at that moment and the
question gets asked out loud. What this codebase relies on instead is that Api::V1::BaseController
does not inherit ApplicationController, so nothing in the API branch has a session helper in scope
to copy from.
When rails new --api is the right answer
Take the flag when the application will never render HTML from Rails, and mean never. A backend for
a mobile client with no web presence, a service whose only consumers are other services, a Rails app
sitting behind a Next.js front end that owns every page a human sees. In those three, the deleted
middleware is deleted weight and the api_only generators stop you accruing a app/views directory
of three stale templates.
Skip the flag when any of these is plausible within a year: an admin screen, a Stripe checkout page,
a password reset that lands on a form, a marketing page on the same domain, a status page, an
OmniAuth callback. Every one of those wants cookies, and most want a template. A full Rails app that
answers JSON from ActionController::API controllers gives up seven middleware entries of
performance it was not going to notice, and keeps the option.
What would change this position: a team where the front end and the back end are owned by different
people with different deploy cadences. At that point the second deployment already exists as an
organisational fact, and building the Rails side as api_only stops pretending otherwise. The
argument for the full app is an argument about a small team, and it stops holding when the team
stops being small.
What this page does not cover
CORS beyond the fact that rails new --api writes a fully commented config/initializers/cors.rb
and a commented gem "rack-cors". Getting the origins, the preflight and the credentials flag right
is a page of its own, and it is only needed once a browser on another origin is a client.
Engines. rails plugin new --api writes config.generators.api_only = true into the engine class
rather than config.api_only, which is the generator half with none of the middleware half, and the
mounting application decides the stack.
Serializers and pagination. Both are decisions an api_only application and a full one make
identically, so --api has no opinion about either.
Which namespace the endpoints sit in, and what a client sees when you cut a v2.
Rails API versioning takes the Api::V1
namespace quoted above and argues the three strategies and the error shape they force. Nor does this
page cover how any of it is written down for a consumer, which is
API documentation generated from the request specs.
rails new --apiis a decision about the next three years taken in the first ten seconds of a project, which is a bad moment to take it. The flag is easy to read as "the JSON one", and what it actually does is remove a specific list of things, some of which are cheap to add back and some of which you will add back badly.Everything below is Rails 8.1.3.1, read out of
railties-8.1.3.1andactionpack-8.1.3.1rather than recalled.The flag writes one line, and the rest follows from it
rails new --apiputsconfig.api_only = trueinto your generatedconfig/application.rb. The template that writes it,lib/rails/generators/rails/app/templates/config/application.rb.tt, sits the line under a three-line comment: "Only loads a smaller set of middleware suitable for API only apps. Middleware like session, flash, cookies can be added back manually. Skip views, helpers and assets when generating a new resource."The setter does more than store the value. In
Rails::Application::Configuration:So one assignment reaches three subsystems: the middleware stack reads
config.api_onlywhile it builds, the generators get retuned throughRails::Generators.api_only!, and the debug exception format flips to:api, which is why a development error in an api_only app comes back as text rather than as the familiar HTML error page with the source extract.The flag is also not a meta option, and the Rails source says so in a comment on
Rails::Generators::AppBase#imply_options: "In contrast, --api is not a meta option because it does other things besides implying options such as --skip-asset-pipeline." What it does imply is--skip-asset-pipelineand--skip-javascript, and--skip-javascriptin turn implies--skip-hotwire.The middleware an api_only stack never builds
Rails::Application::DefaultMiddlewareStack#build_stackis a single method with sevenunless config.api_onlyguards in it, and reading them is the honest version of what the flag removes:Seven entries, and the interesting thing is which ones are not on the list.
ActionDispatch::Staticstill runs, so an api_only app still serves whatever is inpublic/.Rack::ETagandRack::ConditionalGetstill run, so conditional GET works on JSON exactly as it does on HTML.ActionDispatch::RemoteIp,ActionDispatch::RequestId,ActionDispatch::ExecutorandRails::Rack::Loggerare all untouched. The stack is thinner by seven, not by half.Rack::MethodOverrideis the one people forget they lost. It is the middleware that turns a form POST carrying_method=DELETEinto a DELETE, and an API served to a JavaScript client that sends real verbs never notices it is gone. The day somebody adds one HTML form to the same application,form_with method: :deleteposts, and the router answers with a routing error for POST on a path that only accepts DELETE.The controller modules
ActionController::APIleaves outActionController::APIandActionController::Baseboth declare aMODULESconstant, and the difference between the two arrays is the whole story.Baseincludes 36 modules;APIincludes 17.What
APIhas thatBasedoes not:ApiRenderingandBasicImplicitRender, two modules.What
Basehas thatAPIdoes not, all 21 of them:AbstractController::Translation,AbstractController::AssetPaths,Helpers,ActionView::Layouts,Rendering,EtagWithTemplateDigest,EtagWithFlash,MimeResponds,ImplicitRender,ParameterEncoding,Cookies,Flash,FormBuilder,RequestForgeryProtection,ContentSecurityPolicy,PermissionsPolicy,AllowBrowser,Streaming, and the threeHttpAuthenticationcontroller method modules for Basic, Digest and Token.Three of those are worth naming individually because they are the ones that surprise people.
AbstractController::Translationis what defines thetandlshortcuts in a controller, sot("api.errors.invalid_credentials")raisesNoMethodErrorin anActionController::APIsubclass and you writeI18n.tin full.MimeRespondsis what definesrespond_to do |format|, which is the one Rails names in its own documentation as the thing you are most likely to include back. AndHttpAuthentication::Token::ControllerMethodsis what gives youauthenticate_with_http_token, so an API controller that wants a bearer token parses theAuthorizationheader itself.The class documentation states the boundary plainly: an API controller "doesn't include a number of features that are usually required by browser access only: layouts and templates rendering, flash, assets, and so on", and "Request, response, and parameters objects all work the exact same way as ActionController::Base."
The 204 that means somebody forgot to render
ActionController::BaseincludesImplicitRender, which looks for a template matching the action and raisesActionController::MissingExactTemplatewhen there is none.ActionController::APIincludesBasicImplicitRenderinstead, and the quoted block below is the whole of it:An action that falls off the end without rendering answers
204 No Content. Rails documents it as a rule to follow rather than as a hazard: "you need to ensure your controller is calling eitherrenderorredirect_toin all actions, otherwise it will return204 No Content."Consider what that means on a guard clause. A
before_actionthat returns early without rendering, anifbranch with noelse, arescuethat logs and swallows: each one produces a 204, and 204 is a success.response.ok?is false,response.successful?is true,fetchresolves, and most client libraries report it as a win with an empty body. UnderActionController::Basethe same bug is a 500 with an exception name in it. This is the trade the thinner stack makes: it cannot tell a deliberate empty response from a missing one, because it has no template to look for.Generators, and the files
rails new --apideletesRails::Generators.api_only!is whatconfig.api_only = truetriggers, and the whole method is this:So
rails generate scaffold Postin an api_only app produces a controller and a model and no views, no helper and no stylesheet. The mailer keeps ERB explicitly, on the last two lines, because an API that sends no email is not what anybody meant.The generator also deletes, once, at creation time.
AppGeneratorremovesapp/assetsandapp/helpersentirely, removesapp/views/layouts/application.html.erbandapp/views/pwa, removesconfig/initializers/content_security_policy.rb, and createsconfig/initializers/cors.rbwith every line commented out andgem "rack-cors"commented out in the Gemfile to match.app/viewsitself survives as long as Action Mailer does, which is how the mailer templates keep a home.Then it removes
public/400.html,public/404.html,public/406-unsupported-browser.html,public/422.html,public/500.html,public/icon.pngandpublic/icon.svg. That last deletion has a consequence nobody mentions, and the next section is it.A 500 that a browser reads as an empty 404
ActionDispatch::PublicExceptionsis the middleware that turns an unhandled exception into a response in production, and it branches on the requested format. For JSON it builds{ status: 500, error: "Internal Server Error" }and serialises it. For HTML it goes looking for a file:In an api_only application
public/500.htmlwas deleted at generation time, so thatelsebranch is the one that runs. A browser pointed at a broken endpoint of a Rails API only application gets404with an empty body and anX-Cascade: passheader, for a request that failed with a 500.The reason this survives so long in a real project is that nothing in a normal test run reaches it. Request specs assert on JSON,
config.consider_all_requests_localis true in development soDebugExceptionshandles the error long beforePublicExceptionssees it, and the format branch only misbehaves fortext/html. The suite stays green. What finds it is an uptime monitor, or a support ticket from somebody who pasted an API URL into a browser bar, and both of them report "your API returns 404" for an endpoint that is actually crashing.Writing to the session raises; reading it returns nil
sessionis available on every controller regardless of stack, becauseActionController::Metalcarriesdelegate :session, to: "@_request". What differs in an api_only application is that no session store was ever inserted into the middleware, so theActionDispatch::Request::Sessionobject is there and disabled.The two halves behave differently.
load_for_write!raisesActionDispatch::Request::Session::DisabledSessionErrorwith the message "Your application has sessions disabled. To write to the session you must first configure a session store".load_for_read!isload! if !loaded? && exists?, and with sessions disabledexists?is false, so nothing loads, the backing hash stays empty, andsession[:user_id]returnsnil.A loud failure on write and a silent
nilon read is the worst pairing for the code people actually write.session[:user_id] = user.idin a sign-in path blows up in the first minute and gets fixed.if session[:return_to]in a redirect path is just a branch that never fires, forever, and it looks exactly like a branch whose condition happens to be false.What it costs to put cookies and sessions back
Rails is explicit that the removals are reversible, and the guide names
Rack::MethodOverride,ActionDispatch::CookiesandActionDispatch::Flashas things you can re-insert. The mechanical part is two lines inconfig/application.rb:Order matters, since the session store reads the cookie jar the previous middleware built, and
ActionDispatch::Flashwill need both of them under it. The controller side needsinclude ActionController::Cookiesfor thecookieshelper, andinclude ActionController::Flashif you wantflash.The part that does not come back is the part you needed.
RequestForgeryProtectionis a controller module, not middleware, so re-insertingActionDispatch::Cookiesand a cookie session store gives you a browser-style session with no CSRF defence attached to it. Nothing warns.protect_from_forgeryis not defined on your controllers until you include the module, and until then every cookie-authenticated endpoint accepts a cross-site POST. An api_only application that grew a login form is the exact shape where that happens, because the login form is the thing that made somebody add the session back in the first place.The honest accounting: three lines of middleware, two includes, one
protect_from_forgerycall, and a security property you have to remember to restore rather than one you inherit. None of that is hard. All of it is easy to get 80 percent of.Why this boilerplate runs
ActionController::APIinside a full Rails appThe JSON API of this boilerplate is not an api_only application. No file in its
config/setsconfig.api_only, itsconfig/application.rbrequiresaction_view/railtieandaction_cable/enginealong with the rest, and it ships 48 controller classes against 30 directories of views. The JSON API is three of those controllers, underapp/controllers/api/v1/.The base controller reaches for
ActionController::APIdirectly:Inheriting from
ActionController::APIrather than fromApplicationControlleris the load-bearing line.ApplicationControllerin this codebase carries session authentication, the onboarding gate, CSRF protection and a layout, and every one of those is wrong for a bearer-token endpoint. Skipping it means the API controllers never inherit a browser callback they then have to remember toskip_before_action, which is the failure mode of the other arrangement: askip_before_actionlist that drifts one line behindApplicationControllerand lets a redirect-to-sign-in leak into a JSON response.What the full application buys is that there is one of it. The marketing site, the admin console at
/admin, the Stripe checkout and/api/v1/userare one router, one deploy, one set of credentials and oneUsermodel. The api_only version of the same product is two applications sharing a database, or one application plus a separate front end, and either way the admin screen you need in week three is a second thing to deploy. For a product sold to one developer who wants to ship this weekend, one deployment wins, and the cost of that position is in the next section.The token half of this arrangement,
Api::Auth::IssueToken,User.authenticate_byand theAuthorizationheader parsing that stands in for the missingauthenticate_with_http_token, is worked through in Rails JWT API authentication. Thegated_by :apiline on the first page of that controller belongs to a different mechanism, and a disabled feature answering 404 explains why the wordprependin it is what keeps a switched-off API from redirecting anonymous callers to a sign-in page.What the full middleware stack costs the JSON endpoints
Every request to
/api/v1/userin this codebase passes throughActionDispatch::Cookies,ActionDispatch::Session::CookieStore,ActionDispatch::Flash,ActionDispatch::ContentSecurityPolicy::Middleware,Rack::MethodOverrideandRack::TempfileReaper, because the application is not api_only and those are in the stack for the HTML half.bin/rails middlewareprints all of them.The cost is real and it is small. The session store does no work when nothing reads or writes the session, and the API controllers never do: a cookie jar that is never touched is parsed lazily and the response carries no
Set-Cookie.Rack::MethodOverridedeclaresALLOWED_METHODS = %w[POST], so it reads nothing at all on the GET and PATCH this API serves. The flash loads from a session that was never loaded. Measured as throughput on a JSON endpoint, six middleware that mostly return immediately is not the thing that decides whether your API is fast.The cost that is not small is conceptual, and it is worth stating rather than waving away. Cookies being present means a future contributor can write
session[:something]in an API controller and it will work, in development and in tests, and the endpoint stops being stateless without anybody deciding that it should. An api_only application getsDisabledSessionErrorat that moment and the question gets asked out loud. What this codebase relies on instead is thatApi::V1::BaseControllerdoes not inheritApplicationController, so nothing in the API branch has a session helper in scope to copy from.When
rails new --apiis the right answerTake the flag when the application will never render HTML from Rails, and mean never. A backend for a mobile client with no web presence, a service whose only consumers are other services, a Rails app sitting behind a Next.js front end that owns every page a human sees. In those three, the deleted middleware is deleted weight and the api_only generators stop you accruing a
app/viewsdirectory of three stale templates.Skip the flag when any of these is plausible within a year: an admin screen, a Stripe checkout page, a password reset that lands on a form, a marketing page on the same domain, a status page, an OmniAuth callback. Every one of those wants cookies, and most want a template. A full Rails app that answers JSON from
ActionController::APIcontrollers gives up seven middleware entries of performance it was not going to notice, and keeps the option.What would change this position: a team where the front end and the back end are owned by different people with different deploy cadences. At that point the second deployment already exists as an organisational fact, and building the Rails side as api_only stops pretending otherwise. The argument for the full app is an argument about a small team, and it stops holding when the team stops being small.
What this page does not cover
CORS beyond the fact that
rails new --apiwrites a fully commentedconfig/initializers/cors.rband a commentedgem "rack-cors". Getting the origins, the preflight and the credentials flag right is a page of its own, and it is only needed once a browser on another origin is a client.Engines.
rails plugin new --apiwritesconfig.generators.api_only = trueinto the engine class rather thanconfig.api_only, which is the generator half with none of the middleware half, and the mounting application decides the stack.Serializers and pagination. Both are decisions an api_only application and a full one make identically, so
--apihas no opinion about either.Which namespace the endpoints sit in, and what a client sees when you cut a v2. Rails API versioning takes the
Api::V1namespace quoted above and argues the three strategies and the error shape they force. Nor does this page cover how any of it is written down for a consumer, which is API documentation generated from the request specs.