What You'll Need
- n8n Cloud or self-hosted n8n instance
- Hetzner VPS or Contabo VPS for hosting your application
- DigitalOcean as an alternative hosting provider
- OpenAI API key (free trial or paid account at openai.com)
- Node.js 16+ installed locally for development
- Basic understanding of REST APIs and JSON
Table of Contents
- Understanding OpenAI's API Structure
- Setting Up Your Environment
- Building Your First Integration
- Streaming Responses for Better UX
- Error Handling and Rate Limiting
- Production Deployment Strategies
- Getting Started
Understanding OpenAI's API Structure
I've spent the last two years working with OpenAI's API across dozens of production workflows, and I can tell you that understanding its architecture is the foundation of everything that follows. The OpenAI API isn't just a simple endpoint—it's a sophisticated system with multiple models, pricing tiers, and behavioral patterns you need to understand to use it effectively.
OpenAI provides several endpoints: the Chat Completions API (which powers ChatGPT), the Embeddings API (for semantic search), the Moderation API (for content filtering), and the legacy Completions API. Most modern applications use the Chat Completions API because it's more powerful, cheaper, and designed for conversational interactions.
The Chat Completions API works by accepting a list of messages in a specific format and returning a model's response. Each message has a role (system, user, or assistant) and content. The system role sets the behavior context, the user role is what your end-user sends, and the assistant role is what the model previously responded with. This conversation history is crucial—it's what gives the API its contextual awareness.
The models themselves evolve constantly. As of my writing this, GPT-4 Turbo offers better reasoning and 128K context windows, while GPT-3.5 Turbo remains the speed-and-cost champion. Choosing the right model depends on your latency requirements, accuracy needs, and budget constraints.
Setting Up Your Environment
Before you integrate anything, you need a proper development environment. I always start by creating a dedicated project directory and initializing Node.js dependencies.
mkdir openai-integration
cd openai-integration
npm init -y
npm install openai dotenv express cors axios
npm install -D nodemon
Next, create a .env file to store your OpenAI API key securely:
OPENAI_API_KEY=sk-your-actual-key-here
NODE_ENV=development
PORT=3000
Never commit your .env file to version control. Add it to your .gitignore:
node_modules/
.env
.env.local
dist/
Now create your main application file, index.js. This will be your entry point:
const express = require('express');
const cors = require('cors');
require('dotenv').config();
const { OpenAI } = require('openai');
const app = express();
app.use(express.json());
app.use(cors());
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
This gives you a basic Express server with the OpenAI client initialized and ready to use.
Building Your First Integration
💡 Fast-Track Your Project: Don't want to configure this yourself? I build custom n8n pipelines and bots. Message me with code SYS3-DEVTO.
Let's build something practical: a simple chat endpoint that accepts user messages and returns GPT responses. This is the pattern you'll use repeatedly across different applications.
Create a file called chatController.js:
const { OpenAI } = require('openai');
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
const conversationHistory = {};
async function chat(req, res) {
try {
const { userId, message, systemPrompt } = req.body;
if (!message) {
return res.status(400).json({ error: 'Message is required' });
}
if (!userId) {
return res.status(400).json({ error: 'User ID is required' });
}
if (!conversationHistory[userId]) {
conversationHistory[userId] = [];
}
conversationHistory[userId].push({
role: 'user',
content: message,
});
const messages = [
{
role: 'system',
content: systemPrompt || 'You are a helpful assistant that provides clear, concise answers.',
},
...conversationHistory[userId],
];
const completion = await openai.chat.completions.create({
model: 'gpt-4-turbo-preview',
messages: messages,
max_tokens: 1000,
temperature: 0.7,
});
const assistantMessage = completion.choices[0].message.content;
conversationHistory[userId].push({
role: 'assistant',
content: assistantMessage,
});
res.json({
response: assistantMessage,
tokens_used: completion.usage.total_tokens,
conversation_length: conversationHistory[userId].length,
});
} catch (error) {
console.error('OpenAI API error:', error);
res.status(500).json({
error: 'Failed to process request',
details: error.message,
});
}
}
async function clearHistory(req, res) {
const { userId } = req.params;
if (conversationHistory[userId]) {
delete conversationHistory[userId];
res.json({ message: `Conversation history cleared for user ${userId}` });
} else {
res.status(404).json({ error: 'No conversation history found for this user' });
}
}
module.exports = { chat, clearHistory };
Now update your index.js to include these routes:
const express = require('express');
const cors = require('cors');
require('dotenv').config();
const { chat, clearHistory } = require('./chatController');
const app = express();
app.use(express.json());
app.use(cors());
app.post('/api/chat', chat);
app.delete('/api/chat/:userId', clearHistory);
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Test this endpoint with curl:
curl -X POST http://localhost:3000/api/chat \
-H "Content-Type: application/json" \
-d '{"userId":"user123","message":"Explain quantum computing in simple terms","systemPrompt":"You are a physics teacher explaining complex concepts simply"}'
The response will include the GPT-generated answer, token count, and conversation length. This conversation history pattern is critical—it allows you to maintain context across multiple requests, creating a stateful chatbot experience.
Streaming Responses for Better UX
One issue with the approach above: users wait for the entire response before seeing anything. In production applications where responses can take 5-10 seconds, this is frustrating. OpenAI supports streaming, which sends response tokens as they're generated, creating a real-time typing effect similar to ChatGPT itself.
Create a new file called streamingController.js:
const { OpenAI } = require('openai');
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
const conversationHistory = {};
async function streamChat(req, res) {
try {
const { userId, message, systemPrompt } = req.body;
if (!message) {
return res.status(400).json({ error: 'Message is required' });
}
if (!userId) {
return res.status(400).json({ error: 'User ID is required' });
}
if (!conversationHistory[userId]) {
conversationHistory[userId] = [];
}
conversationHistory[userId].push({
role: 'user',
content: message,
});
const messages = [
{
role: 'system',
content: systemPrompt || 'You are a helpful assistant.',
},
...conversationHistory[userId],
];
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
let fullResponse = '';
const stream = await openai.chat.completions.create({
model: 'gpt-4-turbo-preview',
messages: messages,
max_tokens: 1000,
temperature: 0.7,
stream: true,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
if (content) {
fullResponse += content;
res.write(`data: ${JSON.stringify({ token: content })}\n\n`);
}
}
conversationHistory[userId].push({
role: 'assistant',
content: fullResponse,
});
res.write(`data: ${JSON.stringify({ done: true, fullResponse })}\n\n`);
res.end();
} catch (error) {
console.error('Stream error:', error);
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
res.end();
}
}
module.exports = { streamChat };
Update your index.js to include the streaming endpoint:
const express = require('express');
const cors = require('cors');
require('dotenv').config();
const { chat, clearHistory } = require('./chatController');
const { streamChat } = require('./streamingController');
const app = express();
app.use(express.json());
app.use(cors());
app.post('/api/chat', chat);
app.post('/api/chat/stream', streamChat);
app.delete('/api/chat/:userId', clearHistory);
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
On the frontend, you'd consume this with JavaScript:
async function streamChatResponse(userId, message) {
const response = await fetch('/api/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId,
message,
systemPrompt: 'You are a helpful assistant',
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullText = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));
if (data.token) {
fullText += data.token;
document.getElementById('response').textContent = fullText;
}
if (data.done) {
console.log('Stream complete');
}
} catch (e) {
// Skip invalid JSON
}
}
}
}
return fullText;
}
document.getElementById('sendBtn').addEventListener('click', async () => {
const userMessage = document.getElementById('input').value;
await streamChatResponse('user123', userMessage);
});
This creates a real-time typing effect that feels responsive and modern.
Error Handling and Rate Limiting
The OpenAI API has rate limits that vary by plan tier. Hitting these limits will return 429 errors. Additionally, the API can timeout, return 500 errors, or reject requests for various policy reasons. Production code must handle these gracefully.
Create a file called apiClient.js with built-in retry logic:
const { OpenAI } = require('openai');
class ResilientOpenAIClient {
constructor(apiKey) {
this.openai = new OpenAI({ apiKey });
this.maxRetries = 3;
this.baseDelay = 1000;
}
async sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async createCompletion(messages, options = {}) {
let lastError;
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
try {
const completion = await this.openai.chat.completions.create({
model: options.model || 'gpt-4-turbo-preview',
messages,
max_tokens: options.maxTokens || 1000,
temperature: options.temperature || 0.7,
timeout: 30000,
...options,
});
return {
success: true,
data: completion,
attempts: attempt + 1,
};
} catch (error) {
lastError = error;
if (error.status === 429) {
const retryAfter = error.response?.headers['retry-after'] || Math.pow(2, attempt);
const delayMs = retryAfter * 1000;
console.log(`Rate limited. Waiting ${delayMs}ms before retry ${attempt + 1}/${this.maxRetries}`);
await this.sleep(delayMs);
} else if (error.status >= 500) {
const delayMs = this.baseDelay * Math.pow(2, attempt);
console.log(`Server error ${error.status}. Waiting ${delayMs}ms before retry`);
await this.sleep(delayMs);
} else if (error.code === 'ECONNABORTED') {
const delayMs = this.baseDelay * Math.pow(2, attempt);
console.log(`Timeout. Waiting ${delayMs}ms before retry`);
await this.sleep(delayMs);
} else {
return {
success: false,
error: error.message,
status: error.status,
attempts: attempt + 1,
};
}
}
}
return {
success: false,
error: lastError.message,
status: lastError.status,
attempts: this.maxRetries,
};
}
async createStreamCompletion(messages, options = {}) {
let lastError;
for (let attempt = 0; attempt < this.maxRetries; attempt++) {
try {
const stream = await this.openai.chat.completions.create({
model: options.model || 'gpt-4-turbo-preview',
messages,
max_tokens: options.maxTokens || 1000,
temperature: options.temperature || 0.7,
stream: true,
timeout: 30000,
...options,
});
return {
success: true,
data: stream,
attempts: attempt + 1,
};
} catch (error) {
lastError = error;
if (error.status === 429) {
const retryAfter = error.response?.headers['retry-after'] || Math.pow(2, attempt);
const delayMs = retryAfter * 1000;
await this.sleep(delayMs);
} else if (error.status >= 500) {
const delayMs = this.baseDelay * Math.pow(2, attempt);
await this.sleep(delayMs);
} else {
return {
success: false,
error: error.message,
status: error.status,
attempts: attempt + 1,
};
}
}
}
return {
success: false,
error: lastError.message,
status: lastError.status,
attempts: this.maxRetries,
};
}
}
module.exports = ResilientOpenAIClient;
Now update your controllers to use this resilient client:
const ResilientOpenAIClient = require('./apiClient');
const client = new ResilientOpenAIClient(process.env.OPENAI_API_KEY);
async function chat(req, res) {
try {
const { userId, message, systemPrompt } = req.body;
if (!message) {
return res.status(400).json({ error: 'Message is required' });
}
if (!userId) {
return res.status(400).json({ error: 'User ID is required' });
}
if (!conversationHistory[userId]) {
conversationHistory[userId] = [];
}
conversationHistory[userId].push({
role: 'user',
content: message,
});
const messages = [
{
role: 'system',
content: systemPrompt || 'You are a helpful assistant.',
},
...conversationHistory[userId],
];
const result = await client.createCompletion(messages, {
model: 'gpt-4-turbo-preview',
maxTokens: 1000,
temperature: 0.7,
});
if (!result.success) {
return res.status(500).json({
error: 'Failed to generate response',
details: result.error,
status: result.status,
});
}
const assistantMessage = result.data.choices[0].message.content;
conversationHistory[userId].push({
role: 'assistant',
content: assistantMessage,
});
res.json({
response: assistantMessage,
tokens_used: result.data.usage.total_tokens,
attempts: result.attempts,
});
} catch (error) {
console.error('Chat error:', error);
res.status(500).json({
error: 'Internal server error',
details: error.message,
});
}
}
module.exports = { chat };
This pattern ensures that temporary failures don't immediately break your application—you'll retry intelligently with exponential backoff.
Production Deployment Strategies
Deploying to production requires more than just uploading code. I typically deploy Node.js applications using Hetzner VPS or Contabo VPS for cost-effectiveness, though DigitalOcean is also excellent for managed simplicity.
First, set up a proper process manager. Install PM2:
npm install -g pm2
Create an ecosystem.config.js file for PM2:
module.exports = {
apps: [
{
name: 'openai-api',
script: './index.js',
instances: 4,
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000,
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
max_memory_restart: '500M',
merge_logs: true,
},
],
};
On your VPS, create the logs directory and deploy:
mkdir -p logs
pm2 start ecosystem.config.js
pm2 save
pm2 startup
For security and professional deployment, you'll want a reverse proxy. If you're using a Caddy reverse proxy on Ubuntu VPS, your Caddyfile would look like this:
api.yourdomain.com {
reverse_proxy localhost:3000 {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Proto https
}
log {
output file /var/log/caddy/access.log
format json
}
ratelimit /api/* {
rate 100/m
}
}
This provides HTTPS, logging, and rate limiting at the reverse proxy level.
For monitoring and alerting, create a simple health check script:
const axios = require('axios');
async function monitorHealth() {
try {
const response = await axios.get('http://localhost:3000/health', {
timeout: 5000,
});
if (response.status === 200) {
console.log('Health check passed:', new Date().toISOString());
}
} catch (error) {
console.error('Health check failed:', error.message);
// Send alert (email, Slack, PagerDuty, etc)
}
}
setInterval(monitorHealth, 60000);
Save this as health-monitor.js and add it to your PM2 config or cron jobs.
Integrating with n8n for Advanced Workflows
If you want to connect your OpenAI integration to your broader automation infrastructure, n8n Cloud provides native OpenAI support. You can build workflows that trigger on webhooks, process data through OpenAI, and then take actions like sending results via email, saving to databases, or updating CRMs—all without writing additional backend code.
Additionally, if you're building something like a production-ready Telegram bot, you can wire your OpenAI API directly into the bot's message handler, creating an intelligent bot that uses GPT for responding to user queries in real time.
For data management, consider deploying self-hosted Baserow on Ubuntu to store conversation histories, user metadata, and API usage logs alongside your OpenAI integration—creating a complete conversational AI platform.
Getting Started
You now have everything needed to integrate OpenAI's GPT models into a production application. Start by setting up your environment with the code examples above, test locally with curl or Postman, then deploy to Hetzner VPS, Contabo VPS, or DigitalOcean using PM2 and a reverse proxy.
The pattern is consistent across applications: initialize the OpenAI client, maintain conversation history per user, implement streaming for better UX, add resilient error handling with retries, and deploy behind a reverse proxy with monitoring.
Outsource Your Automation
Don't have time to build all this from scratch? I build production n8n workflows, WhatsApp bots, and fully automated YouTube Shorts pipelines—including custom OpenAI integrations tailored to your specific needs. Hire me on Fiverr—mention SYS3-DEVTO for priority. Or DM me directly at chasebot.online.
Originally published on Automation Insider.
Top comments (0)