Using REST Endpoints with a React App
Problem
How to use platformOS as a REST backend for your React app? The goal is to easily define new endpoints for your React app and have your app ready for local development communication with your Instance via proxy.
This article is based on React version 17.
To follow this article, you should have your development environment and a platformOS Instance set up, be familiar with pages, layouts, and React.
Solution
Step 1: Create place for the React app files
The source files of a web app are not part of platformOS and can be stored anywhere you want.
In this example, to keep everything together in one repo and one project, you'll create the React app directly in your Instance's folder but not inside the /app
folder. It is not recommended to deploy your source files to the server, only the built and minified ones.
Navigate to your Instance folder and create the new app with:
npx create-react-app react-app
Instead of react-app
, you can also use another name for your project.
Now you have the /app
, modules
, and react-app
folders in your local Instance.
Step 2: Create your React build as your platformOS page
You can find package.json
under react-app
. You need to add three new scripts:
- a layout script to copy the built
index.html
to your platformOS app's index page - one to clear the
assets
folder - and one to use the built folder as the
assets
"layout": "cp ./build/index.html ../app/views/pages/index.html.liquid",
"assets:remove": "rm -rf ../app/assets/web-app",
"assets:copy": "mv ./build/ ../app/assets/web-app"
You can now use these scripts in your build
script:
"build": "react-scripts build && npm run layout && npm run assets:remove && npm run assets:copy"
Step 3: Set up your app's public folder
platformOS serves static assets from a CDN, so you need to set up your Instance's Public Folder with Environment Variables
For this you need to set up the PUBLIC_URL
variable in your React app's .env
file under the app's root folder:
PUBLIC_URL=https://uploads.staging.oregon.platform-os.com/instances/[INSTANCE ID]/assets/
How to get your-uploads-path
?
The asset_url
filter can be used to fetch the root folder for serving assets.
{{ "" | asset_url }}
To obtain it run pos-cli gui serve dev
to run gui serve
on your dev
enviornment.
Then go to http://localhost:3333/gui/liquid - this is the place where you can evaluate any Liquid Markup against your instance.
For our documentation site it returned: https://uploads.prod01.oregon.platform-os.com/instances/1/assets/
, and yours will be similar, depending on the region and instance ID.
We strongly recommend placing all generated assets in a subfolder, e.g., app/assets/web-app
. Doing so ensures no future conflicts once additional, unrelated assets are added to the project (like graphic files used in emails or a separate admin panel app.)
To test it, build your React app and deploy your Instance.
/react-app$ npm run build
/$ pos-cli deploy dev
It should result in the default create-react-app page as your Instance's home page.
Step 4: Prepare your endpoints' headers
Your endpoints will serve content as JSON. You need to create a Liquid file under app/views/partials/shared/api/headers.liquid
to render the necessary headers:
{
"Content-Type": "application/json"
}
Step 5: Create your Table
You can define your data records under app/schema/
. In this example, you will store basic book infos so create app/schema/book.yml
:
name: book
properties:
- name: title
type: string
- name: subtitle
type: string
Step 6: Create a REST endpoint to store a new book
Create GraphQL mutations to store your data. Place these GraphQL files in the app/graphql/
folder.
To add a new book, create a new file app/graphql/books/create_book.graphql
:
mutation($title: String!, $subtitle: String) {
book: record_create(
record: {
table: "book"
properties: [
{ name: "title", value: $title }
{ name: "subtitle", value: $subtitle }
]
}
) {
id
}
}
The next step is to create a new page used as a REST endpoint for your React application. We recommend storing all endpoints used in the API inside the app/views/pages/api
folder to ensure separation from other site sections.
Additionally, when planning to use different request methods for one URL like POST /api/books
, GET /api/books
, etc., we suggest naming the files after the request method related to the given endpoint. Place the endpoint responsible for adding new books in app/views/pages/api/books/post.liquid
.
---
slug: api/books
method: post
layout: ''
response_headers: >
{% render 'shared/api/headers' %}
---
{% liquid
graphql result = 'books/create_book', title: context.params.title, subtitle: context.params.subtitle
unless result.book
response_status 400
endunless
%}
{{ result }}
You’ve defined the endpoint’s slug and method and removed the default layout. The previously created headers.liquid
file is used to render the appropriate response header.
You’re calling the books/create_book
GraphQL mutation using the request parameters. All GraphQL results always return valid JSON. On error, the result.book
key will not be present, and you should add an appropriate error response status code. Finally, render the whole result
object. It will contain either the new book id or an error message when creating the book has failed.
Step 7: Use your REST endpoint in your React app
The API endpoint is ready. The next step is to add a book form in the /react-app/src/App.jsx
file. In this example, you will use react-hook-form to implement the POST request. In the end, the created book's ID will be logged to the JavaScript console.
We recommend using axios as it allows you to easily add the required csrf-token
to every request.
axios.defaults.xsrfCookieName = 'CSRF-TOKEN';
axios.defaults.xsrfHeaderName = 'X-CSRF-Token';
/react-app$ npm install axios --save
import React from 'react';
import { useForm } from 'react-hook-form';
import axios from 'axios';
import logo from './logo.svg';
import './App.css';
axios.defaults.xsrfCookieName = 'CSRF-TOKEN';
axios.defaults.xsrfHeaderName = 'X-CSRF-Token';
function App() {
const { register, handleSubmit, errors, formState } = useForm();
async function onSubmit(data) {
try {
const response = await axios.post('/api/books', data);
console.log(response);
} catch (err) {
console.error(err);
}
}
return (
<div>
<h1>Save a new book</h1>
<div>
<form onSubmit={handleSubmit(onSubmit)}>
<input
name="title"
placeholder="Title"
ref={register({
required: 'Required',
})}
/>
<label htmlFor="title">{errors.title && errors.title.message}</label>
<input name="subtitle" placeholder="Subtitle" ref={register()} />
<label htmlFor="subtitle">
{errors.subtitle && errors.subtitle.message}
</label>
<button type="submit" disabled={formState.isSubmitting}>
Save
</button>
</form>
</div>
</div>
);
}
export default App;
Step 8: Set up proxy for local API calls during development
During development, your site will most likely be served from the localhost
domain. You do not want to add CORS headers to our API and open it to external websites due to security concerns. To resolve this issue, you need to use a proxy. You can achieve that by installing http-proxy-middleware
.
/react-app$ npm install http-proxy-middleware --save
Create react-app/src/setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function (app) {
app.use(
'/api',
createProxyMiddleware({
target: 'https://your-platformos-instance.com/',
secure: false,
changeOrigin: true,
})
);
};
After that you can run your local environment with
/react-app$ npm start
and synchronize your platformOS files with
/$ pos-cli sync dev
Your Instance will now handle all API calls.