> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fly.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Rails API-only Applications

Rails [API-only](https://guides.rubyonrails.org/api_app.html) Applications
are composed of two parts: a streamlined Rails server typically serving
JSON responses, and a JavaScript client, typically using a framework
such as React, Vue, or Angular.

## Minimal Dockerfile

Below is a Dockerfile that deploys a
[Create React App](https://create-react-app.dev/) client with a
Rails API server:

```dockerfile theme={null}
# syntax = docker/dockerfile:1

FROM node:slim AS react

RUN npx create-react-app client

RUN cd client; npm run build

FROM ruby:slim AS build

RUN apt-get update &&\
    apt-get install --yes build-essential git

RUN gem install rails
RUN rails new demo --minimal --skip-active-record --api

FROM ruby:slim

COPY --from=build /demo /demo
COPY --from=build /usr/local/bundle /usr/local/bundle

COPY --from=react /client/build /demo/public

WORKDIR demo
ENV RAILS_ENV=production
ENV RAILS_SERVE_STATIC_FILES=true
EXPOSE 3000
CMD bin/rails server
```

Differences from a minimal Rails app:

* A `react` build stage based on the `node:slim` image and consisting
  of two `RUN` steps: one that creates a react app, and one that
  builds it.
* An `--api` flag is added to the `rails new` command.
* A `COPY` statement copies the build artifacts to the demo application's
  public directory.
* `RAILS_SERVE_STATIC_FILES` is set to true.  This is necessary as
  [`[[statics]]`](/reference/configuration#the-statics-sections)
  won't find index.html files at the root.  Even so, adding a statics
  section to the `toml` file is useful for the remainder of the bundled
  assets:

  ```
  [[statics]]
  guest_path = "/demo/public"
  url_prefix = "/"
  ```

If the above is deployed you will see a spinning react logo.

## Example

Following is a example that demonstrates a React client working with
a Rails API server.  First the React client:

```dockerfile theme={null}
# syntax = docker/dockerfile:1

FROM node:slim AS react

RUN npx create-react-app client

RUN node --version > client/.node-version

COPY <<-"EOF" client/src/App.js
import logo from './logo.svg';
import './App.css';
import React, { useState, useEffect } from 'react';

function App() {
  let [versions, setVersions] = useState('loading...');

  useEffect(() => {
    fetch('api/versions')
    .then(response => response.json())
    .then(versions => {
      setVersions(Object.entries(versions)
        .map(([name, version]) => `${name}: ${version}`).join(', ')
      )
    });
  });

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>{ versions }</p>
      </header>
    </div>
  );
}

export default App;
EOF

RUN cd client; npm run build

FROM ruby:slim AS build

RUN apt-get update &&\
    apt-get install --yes build-essential git

RUN gem install rails
RUN rails new demo --minimal --skip-active-record --api

FROM ruby:slim

COPY --from=build /demo /demo
COPY --from=build /usr/local/bundle /usr/local/bundle

COPY --from=react /client/build /demo/public
COPY --from=react /client/.node-version /demo

WORKDIR demo

RUN bin/rails generate controller Api versions

COPY <<-"EOF" app/controllers/api_controller.rb
class ApiController < ApplicationController
  def versions
    render json: { 
      ruby: RUBY_VERSION,
      rails: Rails::VERSION::STRING,
      node: IO.read('.node-version').strip.sub(/^v/, '')
    }
  end
end
EOF

ENV RAILS_ENV=production
ENV RAILS_SERVE_STATIC_FILES=true
EXPOSE 3000
CMD bin/rails server
```

The above captures the node version into a file, and defines a React
hook that fetches version information from the API server and
renders the rotating React logo followed by this version information.

Now the server implementation:

```dockerfile theme={null}
# syntax = docker/dockerfile:1

FROM node:slim AS react

RUN npx create-react-app client

RUN node --version > client/.node-version

COPY <<-"EOF" client/src/App.js
import logo from './logo.svg';
import './App.css';
import React, { useState, useEffect } from 'react';

function App() {
  let [versions, setVersions] = useState('loading...');

  useEffect(() => {
    fetch('api/versions')
    .then(response => response.json())
    .then(versions => {
      setVersions(Object.entries(versions)
        .map(([name, version]) => `${name}: ${version}`).join(', ')
      )
    });
  });

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>{ versions }</p>
      </header>
    </div>
  );
}

export default App;
EOF

RUN cd client; npm run build

FROM ruby:slim AS build

RUN apt-get update &&\
    apt-get install --yes build-essential git

RUN gem install rails
RUN rails new demo --minimal --skip-active-record --api

FROM ruby:slim

COPY --from=build /demo /demo
COPY --from=build /usr/local/bundle /usr/local/bundle

COPY --from=react /client/build /demo/public
COPY --from=react /client/.node-version /demo

WORKDIR demo

RUN bin/rails generate controller Api versions

COPY <<-"EOF" app/controllers/api_controller.rb
class ApiController < ApplicationController
  def versions
    render json: { 
      ruby: RUBY_VERSION,
      rails: Rails::VERSION::STRING,
      node: IO.read('.node-version').strip.sub(/^v/, '')
    }
  end
end
EOF

ENV RAILS_ENV=production
ENV RAILS_SERVE_STATIC_FILES=true
EXPOSE 3000
CMD bin/rails server
```

This consists of an additional `COPY` statement to add the `.node-version`
information to the image, and a controller that returns various
version strings as a JSON object.

## Recap

From a Fly.io perspective what you have is a standard Rails application
with an additional build step.  That build step bundles the client
into static assets (HTML, CSS, and JavaScript).
