Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save esafwan/e84313507f613e77d1aea45d63b746d3 to your computer and use it in GitHub Desktop.

Select an option

Save esafwan/e84313507f613e77d1aea45d63b746d3 to your computer and use it in GitHub Desktop.
React to Frappe Integration Architecture

React to Frappe Integration Architecture

This document outlines the standard pattern for integrating a modern React application (like the one in huf/frontend) with a Frappe backend. It explains how to deploy the React app as a Single Page Application (SPA) within Frappe without strictly relying on library wrappers like frappe-react-sdk or frappe-js-sdk. You can easily swap in react-query, standard fetch, or axios.

1. Routing and Base URLs (Frappe hooks.py)

To serve a React app on a specific base route (e.g., /amuse instead of /huf), Frappe's website routing rules must be configured in hooks.py.

When a user navigates to /amuse/dashboard, Frappe needs to serve the React app's index.html file so that React Router can take over on the client side.

In hooks.py:

website_route_rules = [
    # Catch-all for React app routing
    {"from_route": "/amuse/<path:app_path>", "to_route": "amuse"},
    # Base route
    {"from_route": "/amuse", "to_route": "amuse"},
]

This configuration tells Frappe: "Any request starting with /amuse should render the template amuse.html located in the www/ directory of your app."

2. Directory Structure and Build Process

The standard directory separation involves keeping React code in frontend/ and mapping built files into Frappe's structure (public/ and www/).

Key Directories

  • frontend/: The Vite/React source code.
  • huf/public/frontend/: Where Vite outputs the built JS/CSS/image assets. Frappe makes anything in public/ available at /assets/app_name/.
  • huf/www/: Where the entry HTML file (amuse.html) lives. Frappe's web server renders files here for website routes.

Build Coordination (package.json)

The root package.json coordinates the build by delegating to the frontend folder:

{
  "scripts": {
    "build": "yarn build-frontend",
    "build-frontend": "cd frontend && yarn install && yarn build"
  }
}

Inside frontend/package.json:

{
  "scripts": {
    "build": "tsc -b && vite build --base=/assets/amuse/frontend/ && yarn copy-html-entry",
    "copy-html-entry": "cp ../amuse/public/frontend/index.html ../amuse/www/amuse.html"
  }
}
  1. --base=/assets/amuse/frontend/: Instructs Vite that all generated <script> and <link> tags in the HTML should point to /assets/amuse/frontend/.... This matches how Frappe serves files from the public directory.
  2. copy-html-entry: Takes the built index.html from the public folder and places it into www/amuse.html. Because of the website_route_rules in hooks.py, Frappe will map /amuse directly to this file.

3. Development Proxy via Vite

During development, you run the Vite dev server (e.g., http://localhost:8080). To avoid CORS issues and simulate the production environment where React and Frappe share the same origin, you proxy Frappe routes back to the Frappe server (e.g., http://localhost:8000).

In proxyOptions.ts (used by vite.config.ts):

const proxyOptions = {
  '^/(app|api|assets|files|private|login)(/.*)?': {
    target: 'http://localhost:8000', // Your Frappe dev server
    changeOrigin: true,
    secure: false,
    ws: true, // Crucial for WebSockets
  },
};

This ensures API requests (/api/...) or socket connections hit Frappe seamlessly during development.


4. Connecting and Fetching Data (Without SDKs)

If you drop frappe-react-sdk, you can natively interact with Frappe's REST API using fetch + react-query.

Session Authentication & Cookies

Because the React app is served on the same origin as Frappe (both in dev through the proxy, and in prod via www/amuse.html), you do not need explicit API keys or token passing for logged-in users.

Frappe uses an HTTP-only cookie called sid (Session ID). Standard browser fetch will automatically include this cookie if you set credentials: 'include'.

Handling CSRF

For mutating requests (POST, PUT, DELETE), Frappe requires a CSRF token. Frappe automatically embeds window.csrf_token in standard templates. For SPAs, you can ensure it's available by adding this to your www/amuse.html (or fetching it dynamically from /api/method/frappe.auth.get_logged_user).

Custom Fetch Wrapper (frappeClient.ts):

export const frappeFetch = async (endpoint: string, options: RequestInit = {}) => {
  const isMutation = ['POST', 'PUT', 'DELETE'].includes(options.method?.toUpperCase() || '');
  
  const headers = new Headers(options.headers || {});
  headers.set('Content-Type', 'application/json');
  headers.set('Accept', 'application/json');

  if (isMutation && window.csrf_token) {
    headers.set('X-Frappe-CSRF-Token', window.csrf_token);
  }

  const response = await fetch(endpoint, {
    ...options,
    headers,
    credentials: 'include', // Automatically passes the `sid` cookie for auth
  });

  if (!response.ok) {
    const errorData = await response.json().catch(() => ({}));
    throw new Error(errorData.exc || errorData._server_messages || `HTTP Error ${response.status}`);
  }

  return response.json();
};

Pattern Example (CRUD)

With the wrapper above, you can build a clean interface, perfectly suited for react-query:

1. Create (POST)

const createDocument = (doctype: string, data: any) => {
  return frappeFetch(`/api/resource/${doctype}`, {
    method: 'POST',
    body: JSON.stringify(data),
  });
};

2. Read (GET)

const getDocument = (doctype: string, name: string) => {
  return frappeFetch(`/api/resource/${doctype}/${name}`, { method: 'GET' });
};

3. List (GET with filters)

const listDocuments = (doctype: string) => {
  // Frappe expects filters/fields as JSON strings
  const params = new URLSearchParams({
    fields: JSON.stringify(["name", "title", "status"]),
    filters: JSON.stringify([["status", "=", "Active"]]),
    limit_page_length: "20"
  });
  return frappeFetch(`/api/resource/${doctype}?${params.toString()}`, { method: 'GET' });
};

4. Update (PUT)

const updateDocument = (doctype: string, name: string, data: any) => {
  return frappeFetch(`/api/resource/${doctype}/${name}`, {
    method: 'PUT',
    body: JSON.stringify(data),
  });
};

5. Call Custom RPC (Python Methods)

const callMethod = (methodPath: string, args: any) => {
  return frappeFetch(`/api/method/${methodPath}`, {
    method: 'POST', // Use POST for RPC with arguments
    body: JSON.stringify(args),
  });
};

5. WebSockets (Socket.io)

Frappe runs a realtime Node server using Socket.io. You can easily connect to it directly to listen for document events or custom published events from Python.

import { io } from "socket.io-client";

const setupWebSocket = () => {
  const socket = io(window.location.origin, {
    withCredentials: true, // Passes the `sid` cookie to authenticate the socket
    path: "/socket.io"
  });

  socket.on('connect', () => {
    console.log('Connected to Frappe realtime');
  });

  // Example: Frappe publishes doc updates here
  socket.on('doc_update', (data) => {
    console.log('Document updated:', data);
    // Invalidate react-query cache here
  });

  // Example: Custom event triggered via frappe.publish_realtime() in Python
  socket.on('my_custom_event', (data) => {
    console.log('Custom data:', data);
  });

  return socket;
};

Summary of Independence from SDKs

Because of the shared origin and automatic cookie handling:

  1. You can use standard standard tanstack/react-query using the frappeFetch wrapper.
  2. Form state can be handled exactly as you prefer (e.g., react-hook-form).
  3. Realtime fits smoothly into React useEffect or React contexts.
  4. Your hooks.py cleanly maps the entire React frontend to the /amuse URL.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment