Connect React Frontend with Odoo REST API: Complete Guide
By Braincuber Team
Published on February 2, 2026
Odoo's built-in APIs use XML-RPC and JSON-RPC—protocols that work fine but feel clunky when you're building a modern React application. What if you could just hit a simple REST endpoint and get JSON back? That's exactly what we'll build. By the end of this guide, you'll have a custom Odoo module exposing REST endpoints and a React app consuming them.
This approach is perfect for building headless Odoo applications, custom dashboards, mobile apps, or any scenario where you want a modern JavaScript frontend powered by Odoo data.
- Custom REST API endpoint in Odoo
- React component that fetches Odoo data
- Proper CORS configuration for frontend integration
- Production-ready error handling
Part 1: Create the REST API in Odoo
We'll build a custom module that exposes customer data through a REST endpoint. This same pattern works for products, orders, invoices, or any Odoo model.
Module Structure
First, set up your module folder structure:
customer_api/
├── __init__.py
├── __manifest__.py
└── controllers/
├── __init__.py
└── api.py
The API Controller
This is where the magic happens. We define a controller class with a route that returns JSON data:
from odoo import http
from odoo.http import request, Response
import json
class CustomerApiController(http.Controller):
@http.route('/api/customers', auth='public', type='http',
methods=['GET'], csrf=False, cors="*")
def get_customers(self):
"""
Returns a list of all customers with basic info.
Endpoint: GET /api/customers
"""
customers = request.env['res.partner'].sudo().search_read(
[('customer_rank', '>', 0)],
['id', 'name', 'email', 'phone', 'city', 'country_id']
)
# Format country data
for customer in customers:
if customer.get('country_id'):
customer['country'] = customer['country_id'][1]
del customer['country_id']
else:
customer['country'] = None
return Response(
json.dumps({'status': 'success', 'data': customers}),
content_type='application/json',
status=200
)
@http.route('/api/customers/<int:customer_id>', auth='public',
type='http', methods=['GET'], csrf=False, cors="*")
def get_customer(self, customer_id):
"""
Returns details for a single customer.
Endpoint: GET /api/customers/{id}
"""
customer = request.env['res.partner'].sudo().search_read(
[('id', '=', customer_id)],
['id', 'name', 'email', 'phone', 'street', 'city',
'zip', 'country_id', 'create_date']
)
if not customer:
return Response(
json.dumps({'status': 'error', 'message': 'Customer not found'}),
content_type='application/json',
status=404
)
result = customer[0]
if result.get('country_id'):
result['country'] = result['country_id'][1]
del result['country_id']
return Response(
json.dumps({'status': 'success', 'data': result}),
content_type='application/json',
status=200
)
auth='public'
Makes the API accessible without login. Change to 'user' if you need authentication.
type='http'
Enables standard HTTP responses instead of Odoo's default JSON-RPC format.
csrf=False
Disables CSRF protection—required for external API calls that don't have Odoo session tokens.
cors="*"
Allows Cross-Origin requests from any domain. Essential for React apps running on different ports.
Init Files
# customer_api/__init__.py
from . import controllers
# customer_api/controllers/__init__.py
from . import api
Module Manifest
{
'name': 'Customer REST API',
'version': '18.0.1.0.0',
'category': 'Technical',
'summary': 'REST API endpoints for customer data',
'description': 'Exposes customer data via REST API for React integration',
'author': 'Your Company',
'depends': ['base', 'contacts'],
'data': [],
'installable': True,
'application': False,
'auto_install': False,
'license': 'LGPL-3',
}
Test the Endpoint
After installing the module, test your API with curl:
# Get all customers
curl http://localhost:8069/api/customers
# Get a specific customer
curl http://localhost:8069/api/customers/1
{
"status": "success",
"data": [
{
"id": 1,
"name": "Azure Interior",
"email": "azure@example.com",
"phone": "+1 555-123-4567",
"city": "San Francisco",
"country": "United States"
},
{
"id": 2,
"name": "Deco Addict",
"email": "deco@example.com",
"phone": "+1 555-987-6543",
"city": "Los Angeles",
"country": "United States"
}
]
}
Part 2: Build the React Frontend
Now let's create a React app that consumes our Odoo API. We'll build a customer list with proper loading states and error handling.
Create the React Project
npx create-react-app odoo-customer-dashboard
cd odoo-customer-dashboard
npm start
API Service
Create a dedicated service file to handle API calls—this keeps your components clean:
const ODOO_API_URL = 'http://localhost:8069/api';
export const fetchCustomers = async () => {
try {
const response = await fetch(`${ODOO_API_URL}/customers`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.status !== 'success') {
throw new Error(result.message || 'API returned error status');
}
return result.data;
} catch (error) {
console.error('Failed to fetch customers:', error);
throw error;
}
};
export const fetchCustomerById = async (customerId) => {
try {
const response = await fetch(`${ODOO_API_URL}/customers/${customerId}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.status !== 'success') {
throw new Error(result.message || 'Customer not found');
}
return result.data;
} catch (error) {
console.error('Failed to fetch customer:', error);
throw error;
}
};
Customer List Component
import React, { useEffect, useState } from 'react';
import { fetchCustomers } from '../services/odooApi';
import './CustomerList.css';
const CustomerList = () => {
const [customers, setCustomers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const loadCustomers = async () => {
try {
setLoading(true);
const data = await fetchCustomers();
setCustomers(data);
setError(null);
} catch (err) {
setError('Failed to load customers. Is Odoo running?');
} finally {
setLoading(false);
}
};
loadCustomers();
}, []);
if (loading) {
return (
<div className="loading-container">
<div className="spinner"></div>
<p>Loading customers from Odoo...</p>
</div>
);
}
if (error) {
return (
<div className="error-container">
<p className="error-message">{error}</p>
<button onClick={() => window.location.reload()}>
Retry
</button>
</div>
);
}
return (
<div className="customer-list">
<h2>Odoo Customers</h2>
<p className="customer-count">{customers.length} customers found</p>
<div className="customer-grid">
{customers.map((customer) => (
<div key={customer.id} className="customer-card">
<h3>{customer.name}</h3>
<div className="customer-details">
{customer.email && (
<p>
<span className="label">Email:</span>
{customer.email}
</p>
)}
{customer.phone && (
<p>
<span className="label">Phone:</span>
{customer.phone}
</p>
)}
{customer.city && (
<p>
<span className="label">Location:</span>
{customer.city}{customer.country ? `, ${customer.country}` : ''}
</p>
)}
</div>
</div>
))}
</div>
</div>
);
};
export default CustomerList;
Main App Component
import React from 'react';
import CustomerList from './components/CustomerList';
import './App.css';
function App() {
return (
<div className="App">
<header className="app-header">
<h1>Customer Dashboard</h1>
<p>Powered by React + Odoo</p>
</header>
<main className="app-content">
<CustomerList />
</main>
</div>
);
}
export default App;
Handling CORS in Production
In development, using cors="*" is fine. But for production, you should restrict CORS to your specific domain:
# Development - allows any origin
@http.route('/api/customers', auth='public', type='http',
methods=['GET'], csrf=False, cors="*")
# Production - restrict to your domain
@http.route('/api/customers', auth='public', type='http',
methods=['GET'], csrf=False, cors="https://dashboard.yourcompany.com")
Using auth='public' with cors="*" exposes your data to anyone. For sensitive data, implement proper authentication using auth='user' and handle session tokens in your React app, or use API keys with custom validation.
Adding Authentication (Optional)
For secured endpoints, change the auth parameter and handle authentication in React:
@http.route('/api/customers/secure', auth='user', type='http',
methods=['GET'], csrf=False, cors="*")
def get_customers_secure(self):
"""
Secured endpoint - requires Odoo session authentication.
Returns only customers the current user has access to.
"""
user = request.env.user
customers = request.env['res.partner'].search_read(
[('customer_rank', '>', 0)],
['id', 'name', 'email', 'phone']
)
return Response(
json.dumps({
'status': 'success',
'user': user.name,
'data': customers
}),
content_type='application/json',
status=200
)
You now have a complete React + Odoo integration: a custom REST API module in Odoo that exposes customer data, and a React frontend that fetches and displays that data. This same pattern extends to any Odoo model—products, orders, invoices, inventory—giving you the flexibility to build modern web applications powered by Odoo's robust backend.
Frequently Asked Questions
XML-RPC and JSON-RPC are Odoo's built-in protocols that require specific client libraries and handle authentication through session management. REST APIs are custom endpoints you create using Odoo controllers—they return standard JSON responses and can be consumed by any HTTP client, including JavaScript's fetch(). REST is simpler for modern frontend frameworks like React, Vue, or Angular.
CORS (Cross-Origin Resource Sharing) errors occur when your React app (running on localhost:3000) tries to access your Odoo API (running on localhost:8069). To fix this, add cors='*' to your Odoo route decorator for development, or specify your React app's domain for production. Make sure csrf=False is also set, as React won't have Odoo's CSRF tokens.
Change auth='public' to auth='user' in your route decorator. This requires a valid Odoo session. For React integration, you'll need to first authenticate against Odoo's /web/session/authenticate endpoint to get a session cookie, then include credentials: 'include' in your fetch requests. For API key authentication, implement custom token validation in your controller.
Yes, add the desired methods to your route decorator: methods=['GET', 'POST', 'PUT', 'DELETE']. For POST/PUT requests, access the request body using json.loads(request.httprequest.data). Remember to validate input data and handle permissions appropriately, especially for create/update/delete operations.
Wrap your controller logic in try-except blocks and return appropriate HTTP status codes. Return 200 for success, 400 for bad requests, 404 for not found, and 500 for server errors. Always return JSON with a consistent structure like {status: 'error', message: 'Description'} so your React app can handle errors gracefully.
