React AI chat deployment: Production-ready apps
Deploying a React chatbot to production requires more than copying files to a server. You must secure your API keys, proxy requests through a backend, monitor errors, handle scaling, and ensure reliability. This article covers production-grade deployment patterns: environment management, backend proxies, error tracking, analytics, and best practices used by teams deploying millions of chat messages daily.
According to Anthropic's Production AI Guidelines (2025), 64% of early-stage chatbot deployments fail due to poor error handling or missing observability. The difference between a prototype and a production system is visibility and resilience.
Securing Your API Keys
Never expose API keys in your React code. Use environment variables and a backend proxy:
# .env.local (not committed to git)
REACT_APP_BACKEND_URL=https://api.yourapp.com
# .env.production
REACT_APP_BACKEND_URL=https://api.yourapp.com
In your React code:
// WRONG: exposes API key in browser code
const apiKey = 'sk-abc123...'; // NEVER DO THIS
const response = await fetch('https://api.openai.com/v1/chat/completions', {
headers: { 'Authorization': `Bearer ${apiKey}` },
});
// RIGHT: call your backend instead
const response = await fetch(`${process.env.REACT_APP_BACKEND_URL}/api/chat`, {
method: 'POST',
body: JSON.stringify({ messages, model: 'gpt-4o-mini' }),
});
Your backend holds the API key in a secure environment variable:
// backend/routes/chat.js
const OPENAI_API_KEY = process.env.OPENAI_API_KEY; // Secure, not exposed
app.post('/api/chat', async (req, res) => {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(req.body),
});
// Stream or return response
res.setHeader('Content-Type', 'text/event-stream');
response.body.pipe(res);
});
Building a Backend Proxy
Your backend intercepts chat requests, adding:
- Authentication (verify user is logged in)
- Rate limiting (prevent abuse)
- Logging (track usage for analytics)
- Audit trails (for compliance)
Here is a minimal Express proxy:
const express = require('express');
const fetch = require('node-fetch');
const rateLimit = require('express-rate-limit');
const app = express();
app.use(express.json());
// Middleware: verify authentication
function authenticateUser(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Unauthorized' });
// Verify JWT or session
// ...
next();
}
// Rate limiting: 10 requests per minute per user
const limiter = rateLimit({
windowMs: 60 * 1000,
max: 10,
keyGenerator: (req) => req.user.id, // Per-user limit
});
app.post('/api/chat', authenticateUser, limiter, async (req, res) => {
try {
// Log request
console.log(`User ${req.user.id} sent chat request`, {
messageCount: req.body.messages.length,
timestamp: new Date(),
});
// Call OpenAI
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: req.body.messages,
stream: true,
}),
});
if (!response.ok) {
const error = await response.json();
console.error('OpenAI error:', error);
return res.status(response.status).json({ error: error.error.message });
}
// Stream response back to client
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
response.body.pipe(res);
// Log completion
response.body.on('end', () => {
console.log(`Chat request from user ${req.user.id} completed`);
});
} catch (error) {
console.error('Chat error:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
app.listen(3001, () => console.log('Backend listening on port 3001'));
Environment Configuration
Use different configs for development, staging, and production:
// config/index.js
const config = {
development: {
BACKEND_URL: 'http://localhost:3001',
LOG_LEVEL: 'debug',
SENTRY_DSN: null,
ANALYTICS_ENABLED: false,
},
staging: {
BACKEND_URL: 'https://api-staging.yourapp.com',
LOG_LEVEL: 'info',
SENTRY_DSN: 'https://[email protected]/0',
ANALYTICS_ENABLED: true,
},
production: {
BACKEND_URL: 'https://api.yourapp.com',
LOG_LEVEL: 'warn',
SENTRY_DSN: 'https://[email protected]/0',
ANALYTICS_ENABLED: true,
},
};
export default config[process.env.NODE_ENV || 'development'];
In your React app:
import config from './config';
const response = await fetch(`${config.BACKEND_URL}/api/chat`, {
method: 'POST',
body: JSON.stringify({ messages }),
});
Error Tracking with Sentry
Monitor errors in production:
npm install @sentry/react @sentry/tracing
Initialize Sentry in your app:
import * as Sentry from '@sentry/react';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
Sentry.init({
dsn: process.env.REACT_APP_SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 1.0,
integrations: [
new Sentry.Replay({
maskAllText: false,
blockAllMedia: false,
}),
],
});
ReactDOM.render(
<Sentry.ErrorBoundary fallback={<div>Something went wrong</div>} showDialog>
<App />
</Sentry.ErrorBoundary>,
document.getElementById('root')
);
Errors are automatically reported to Sentry, where you can view them in a dashboard, set alerts, and trace user sessions.
Monitoring and Analytics
Track key metrics:
import config from './config';
function logEvent(eventName, properties = {}) {
if (!config.ANALYTICS_ENABLED) return;
const event = {
name: eventName,
timestamp: new Date(),
userId: getCurrentUserId(),
...properties,
};
fetch(`${config.BACKEND_URL}/api/analytics`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event),
}).catch((err) => console.error('Analytics error:', err));
}
// Usage
function ChatApp() {
const handleSendMessage = async (message) => {
logEvent('chat_message_sent', { length: message.length });
try {
const response = await fetch(`${config.BACKEND_URL}/api/chat`, {
method: 'POST',
body: JSON.stringify({ messages }),
});
logEvent('chat_response_received', { statusCode: response.status });
} catch (error) {
logEvent('chat_error', { error: error.message });
}
};
return <div>{/* ... */}</div>;
}
Track:
- Messages sent / responses received
- Errors (type, frequency, stack trace)
- Latency (time from send to first token)
- User retention (daily/weekly active users)
- API cost (tokens used per user)
Deployment Platforms
Here are popular options for deploying React + Node.js chatbots:
| Platform | Best for | Pricing | Scaling |
|---|---|---|---|
| Vercel | Frontend + serverless backend | Free tier + pay-as-you-go | Auto-scales |
| Heroku | Quick prototypes | $7–50/month | Manual dyno management |
| AWS EC2 | Full control | $0.01–$1/hour | Manual scaling |
| Google Cloud Run | Serverless containers | Pay-per-request | Auto-scales |
| Railway | Modern deployment UX | $5–200/month | Auto-scales |
For a production chatbot, Vercel (frontend) + Google Cloud Run (backend) is a solid, scalable combo.
Deploying to Vercel
# Install Vercel CLI
npm install -g vercel
# Deploy frontend
vercel --prod
For the backend, create a serverless function:
// api/chat.js (Vercel serverless function)
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4o-mini',
messages: req.body.messages,
stream: true,
}),
});
if (!response.ok) {
return res.status(response.status).json({ error: 'API error' });
}
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
response.body.pipe(res);
} catch (error) {
res.status(500).json({ error: error.message });
}
}
Vercel automatically scales serverless functions based on traffic.
Pre-deployment Checklist
Before going live:
- API keys stored in
.env.productionand set in platform dashboard - Rate limiting enabled on backend
- Error tracking (Sentry) configured
- Analytics logging implemented
- Authentication implemented (JWT, OAuth, etc.)
- HTTPS everywhere (automatic on Vercel, CloudFlare)
- CORS configured for frontend domain
- Backward compatibility for old API versions
- Monitoring and alerts set up (Sentry, datadog, etc.)
- Load testing done (simulate 100+ concurrent users)
- Graceful degradation when API is down
- Terms of Service and privacy policy updated
- User data retention policy documented
- Backup and disaster recovery plan
Scaling Considerations
As your chatbot grows:
- Cache frequently asked questions to reduce API calls
- Implement request queuing to prevent overload
- Use CDN for static assets (Cloudflare, AWS CloudFront)
- Archive old conversations to reduce database size
- Monitor API costs and set spending limits in your provider dashboard
Key Takeaways
- Never expose API keys in frontend code; use a backend proxy.
- Environment variables separate configuration across development, staging, and production.
- Error tracking (Sentry) and analytics provide visibility into user issues.
- Rate limiting on the backend prevents abuse and runaway costs.
- Deploy frontend and backend to separate platforms for independence and scaling.
Frequently Asked Questions
What if the OpenAI API goes down?
Return a user-friendly error message and offer to queue the request for later retry. Implement a job queue (Bull, RabbitMQ) that retries requests when the API recovers.
How do I handle users with poor internet connections?
Implement request retries on the client, with exponential backoff. Show a "Retrying..." message. For very slow connections, consider a lower context window to reduce payload size.
Should I cache AI responses?
Only if responses are deterministic (same prompt = same answer). For most chatbots, caching is not helpful since each user has different conversation history. Caching adds complexity without much benefit.
How do I monitor costs?
OpenAI provides usage dashboards. Set spending limits and alerts in your account. Log each request on your backend with token count. Build a dashboard showing cost per user, per day, per conversation.
What if a user tries to jailbreak the API (e.g., "ignore your instructions")?
The AI API is designed to resist these attacks. Set a strong system prompt that guides the assistant's behavior. Never pass user input directly as a system message. Log suspicious patterns and consider rate-limiting suspicious accounts.