Most developers who integrate AI chat into their applications start with a pre-built widget or a simple script that calls an AI model directly. These approaches work for prototypes but quickly break in production. Third-party chat widgets often lock you into their UI, limit customization, and expose your API keys in the browser. A custom AI chat endpoint gives you full control over the request flow, authentication, message history, and response handling—whether you're building a customer support bot, an internal SaaS assistant, or a tool for automating routine queries. In this tutorial, you'll build a dedicated Node.js endpoint that accepts user messages and returns AI-generated responses with real-time streaming. The endpoint will follow a clean REST design: a single POST route that receives a message and optional conversation history, then communicates with the Telnyx AI Assistants API to generate replies. You'll also learn how to stream those responses back to the client using Server-Sent Events, giving users a smooth, typewriter-style experience. By the end, you'll have a production-ready foundation you can extend with authentication, rate limiting, and your own business logic.
Prerequisites and Project Setup
Before writing any code, you need the right environment and credentials. This section covers everything from Node.js version requirements to pulling your Telnyx API keys.
Node.js version (v18+)
The code in this tutorial relies on modern JavaScript features (like native fetch) and the Telnyx SDK’s streaming APIs. Make sure you have Node.js 18 or higher installed. You can check your version with node --version. If you need to upgrade, use nvm or download the latest LTS from nodejs.org.
Get your Telnyx API Key and Assistant ID
- Sign in to your Telnyx Mission Control account.
- In the left sidebar, go to AI Assistants and click Create Assistant (or select an existing one).
- Copy the Assistant ID from the assistant overview page.
- Generate an API key: navigate to API Keys under the Developers section, create a new key, and copy it. Treat this key like a password.
Initialize the project
Create a new directory and initialize a Node.js project:
mkdir ai-chat-endpoint
cd ai-chat-endpoint
npm init -y
Set environment variables
Create a .env file in the project root with the values you just copied:
TELNYX_API_KEY=your_telnyx_api_key_here
ASSISTANT_ID=your_assistant_id_here
We’ll load these with the dotenv package so sensitive keys never end up in your codebase.
Install dependencies
Run the following command to install the core libraries:
npm install express telnyx dotenv cors
- express – the web framework for handling HTTP requests
- telnyx – official Node.js SDK for the Telnyx API
-
dotenv – loads
.envvariables intoprocess.env - cors – enables cross-origin requests (needed when a frontend from a different domain talks to your endpoint)
Project folder structure
For clarity, organize your files like this:
ai-chat-endpoint/
├── .env
├── package.json
├── src/
│ ├── index.js # Express server entry point
│ ├── routes/
│ │ └── chat.js # Chat endpoint handler
│ └── middleware/
│ └── errorHandler.js # Centralised error handling
└── .gitignore (add .env)
With everything in place, you’re ready to build the Express server and wire up the Telnyx AI Assistants API.
Setting Up the Express Server and Middleware
With your project initialized and dependencies installed, the next step is to create a robust Express server. This server will handle incoming requests, apply security headers, parse JSON payloads, and manage cross-origin requests—all essential for a production-grade AI chat endpoint.
Start by creating an index.js file in your project root. Import Express and initialize the app:
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const app = express();
const PORT = process.env.PORT || 3000;
Port configuration via environment variable (PORT) is a best practice—it allows your deployment platform (e.g., Heroku, Render, or a Docker container) to assign the port dynamically. Fall back to 3000 for local development.
Next, apply middleware in the correct order:
Helmet for security headers. This sets various HTTP headers (like
X-Content-Type-OptionsandX-Frame-Options) to protect against common web vulnerabilities.CORS with a whitelist of allowed origins. In production, avoid using a wildcard (
*). Instead, specify the exact domains that can call your endpoint, such as your frontend application.JSON body parser with a size limit. This prevents large payloads from overwhelming your server. A typical limit for chat messages is 1 MB.
app.use(helmet());
const allowedOrigins = ['https://your-frontend.com', 'http://localhost:5173'];
app.use(cors({
origin: function (origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
}
}));
app.use(express.json({ limit: '1mb' }));
Production considerations: You may also want to add rate limiting middleware (e.g., express-rate-limit) to prevent abuse, and body size limits to reject oversized requests. These will be covered in more detail in Section 6.
Finally, add a health check endpoint at /health that returns a simple status. This is useful for load balancers and monitoring tools:
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});
Start the server by listening on the configured port:
app.listen(PORT, () => {
console.log(`AI chat server running on port ${PORT}`);
});
With this foundation, you have a secure, configurable Express server ready to accept chat requests. The next section will wire up the core chat endpoint using the Telnyx AI Assistants API.
Building the Core Chat Endpoint with Telnyx AI Assistants
With the Express server and middleware in place, we can now implement the primary chat endpoint. This POST route will accept a user message and an optional message history, validate the input, and forward the request to the Telnyx AI Assistants API.
Request Body Schema
The endpoint expects a JSON body with the following fields:
-
message(string, required): The user’s current message. -
history(array, optional): An array of previous chat messages, each containingrole("user" or "assistant") andcontent(string). This allows the AI to maintain conversation context.
Input Validation
Always validate incoming requests to prevent malformed data from reaching the API. In the route handler, check that message exists and is a non-empty string. If validation fails, return a 400 response with a clear error message.
app.post('/api/chat', async (req, res) => {
const { message, history } = req.body;
if (!message || typeof message !== 'string' || message.trim() === '') {
return res.status(400).json({ error: 'A valid message is required.' });
}
// ... continue
});
Initializing the Telnyx Client
In your project, you already stored the Telnyx API key in the environment variable TELNYX_API_KEY. Create a Telnyx client instance at the top of your route file using the telnyx package:
const Telnyx = require('telnyx')(process.env.TELNYX_API_KEY);
Calling the AI Assistants API
Now build the messages array by combining the history (if provided) and the new user message. Then call Telnyx.ai.assistants.messages.create() with the required parameters:
-
assistant_id: The ID of your Telnyx AI Assistant (stored inprocess.env.ASSISTANT_ID). -
messages: The combined conversation history plus the current user message. -
model: The model to use (e.g., "gpt-4o" or your assistant’s default).
try {
const messages = [
...(Array.isArray(history) ? history : []),
{ role: 'user', content: message }
];
const response = await Telnyx.ai.assistants.messages.create({
assistant_id: process.env.ASSISTANT_ID,
messages: messages,
model: 'gpt-4o'
});
res.status(200).json({ reply: response.data.choices[0].message.content });
} catch (error) {
console.error('Telnyx API error:', error);
res.status(500).json({ error: 'Failed to get AI response.' });
}
Notice the structured error handling: if the API call fails, we log the error and return a 500 status with a generic message. This prevents exposing internal details to the client while still allowing you to debug.
Complete Endpoint Example
Putting it all together, the full route handler includes validation, Telnyx client usage, async error handling, and a clean JSON response. You can now test it with a POST request containing a valid message field. The endpoint will return the AI’s reply in the reply property. This forms the core of your AI chat API, ready to be extended with streaming and more advanced features in the next section.
Adding Streaming Support for Real-Time Responses
To deliver the conversational experience users expect from an AI chat application, you need real-time responses. In this section, you will extend the core chat endpoint to stream responses back to the client using Server-Sent Events (SSE). SSE is a lightweight, standard protocol that opens a single long-lived HTTP connection and pushes text events from server to client.
Start by creating a new route, typically POST /api/chat/stream, or add a ?stream=true query parameter to the existing endpoint. The critical change is to tell the Telnyx AI Assistants API that you want a streaming response by setting the Accept header to text/event-stream when calling the API. This instructs Telnyx to return the response incrementally rather than as a single JSON payload.
Next, configure the Express response headers for SSE. Before any data is written, set:
Content-Type: text/event-streamCache-Control: no-cacheConnection: keep-alive
Once the headers are sent and the Telnyx stream begins, read the incoming chunks using the streaming interface provided by the Telnyx Node.js SDK. For each chunk that contains a choices[0].delta.content field, write it to the res object using the SSE format: data: <chunk-content>\n\n. Flush the response after each chunk to ensure the client receives it immediately.
A crucial production concern is handling client disconnects. When the user closes the browser or aborts the request, the server must stop streaming to avoid wasting resources and generating errors. Listen for the close event on the req object. In the event handler, cleanly destroy the Telnyx stream and call res.end(). Optionally, you can also implement a timeout to force-close idle streams.
Comparing performance profiles, the streaming endpoint offers distinct advantages. A non-streaming endpoint has a single latency spike equal to the model’s full generation time plus network round trips. In contrast, the streaming endpoint achieves lower perceived latency because the client receives the first token almost immediately, and subsequent tokens arrive as they are generated. This drastically improves user experience, especially for longer responses, and allows the client to render text progressively.
Here is a rough code outline (assuming an existing Telnyx client instance telnyx and a properly validated messages array):
app.post('/api/chat/stream', async (req, res) => {
const { messages } = req.body;
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
});
try {
const stream = await telnyx.ai.chat.completions.create({
model: 'your-model-id',
messages,
stream: true,
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
if (content) {
res.write(`data: ${JSON.stringify({ content })}\n\n`);
}
}
res.write('data: [DONE]\n\n');
} catch (err) {
res.write(`data: ${JSON.stringify({ error: err.message })}\n\n`);
} finally {
res.end();
}
});
Remember to implement cleanup on client disconnect, especially in a production environment. With streaming in place, your chatbot feels responsive and modern—similar to the real-time experiences you see in Paradane’s own AI-powered applications.
Error Handling, Rate Limiting, and Monitoring
A production chat endpoint must gracefully handle failures. Errors fall into three categories: authentication failures (invalid API key), rate limits, and upstream model errors or timeouts. Each requires a distinct HTTP status code: 401 for authentication, 429 for rate limits, and 500 for server or model errors.
Start by installing express-rate-limit to protect your endpoint from abuse:
npm install express-rate-limit
Then configure it in your server file:
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per window
standardHeaders: true,
legacyHeaders: false,
});
app.use('/chat', limiter);
Use a centralized error-handling middleware to capture and classify exceptions. For example, when the Telnyx client throws an authentication error, return a 401 response. When the API responds with a 429 status, pass that code through. For generic failures or timeouts, use 500. Attach a unique request ID (generated via uuid) to every log entry to simplify debugging.
app.use((err, req, res, next) => {
const requestId = req.id || 'unknown';
console.error(`[${requestId}] Error:`, {
message: err.message,
status: err.status || 500,
// Do not log req.body or API keys
});
const statusCode = err.status || 500;
res.status(statusCode).json({
error: statusCode === 429 ? 'Too many requests' : 'Internal server error',
requestId,
});
});
Never expose API keys or raw error stacks in production responses. Use a logging library like pino or winston with structured JSON output, and forward logs to a monitoring service such as Sentry for error tracking, PostHog for usage analytics, or a custom dashboard. Proper error handling and observability turn your chat endpoint from a prototype into a reliable service. Paradane partners with teams to implement these production best practices and can help you build observability into your full-stack AI applications.
Testing Your AI Chat Endpoint and Next Steps
Now that you have a functional endpoint with streaming, it's time to verify everything works. Start with a simple non-streaming request using curl. Replace $API_URL with your server's URL, and $ASSISTANT_ID with your actual Telnyx assistant ID:
curl -X POST $API_URL/chat \
-H "Content-Type: application/json" \
-d '{"message": "Hello, how does this chat work?", "assistant_id": "$ASSISTANT_ID"}'
You should receive a JSON response with the AI's reply. To test streaming, add the Accept: text/event-stream header:
curl -N -X POST $API_URL/chat/stream \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"message": "Write a short poem about Node.js", "assistant_id": "$ASSISTANT_ID"}'
Watch the response appear token by token. For a more interactive test, create a minimal HTML page:
<!DOCTYPE html>
<html>
<body>
<div id="chat"></div>
<input id="input" placeholder="Type a message...">
<button onclick="send()">Send</button>
<script>
async function send() {
const msg = document.getElementById('input').value;
const chat = document.getElementById('chat');
chat.innerHTML += `<p><b>You:</b> ${msg}</p>`;
const resp = await fetch('http://localhost:3000/chat/stream', {
method: 'POST',
headers: {'Content-Type': 'application/json', 'Accept': 'text/event-stream'},
body: JSON.stringify({message: msg, assistant_id: 'YOUR_ASSISTANT_ID'})
});
const reader = resp.body.getReader();
const decoder = new TextDecoder();
chat.innerHTML += '<p><b>AI:</b> ';
while (true) {
const {done, value} = await reader.read();
if (done) break;
const text = decoder.decode(value, {stream: true});
chat.innerHTML += text.replace(/^data: /gm, '');
}
chat.innerHTML += '</p>';
}
</script>
</body>
</html>
Open this file in a browser and start chatting. Your endpoint responds in real time.
Next Steps for Production Readiness
With a working endpoint, consider enhancing it for real-world use:
- Context management: Pass conversation history as an array to maintain coherent multi-turn dialogues.
-
Multi-model support: Allow users to switch between different Telnyx AI Assistants or models via a
modelparameter in the request. - Analytics: Log request duration, token count, and user sessions to understand usage patterns and optimize costs.
- Authentication: Add API key validation or JWT tokens to secure your endpoint.
To see how this fits into a complete application, try integrating your chat endpoint into a customer support dashboard or a personal assistant project. For teams needing to accelerate development of production-grade chat features or full-stack AI applications, explore how Paradane can help at https://paradane.com. They specialize in turning API tutorials like this into scalable, maintainable solutions.
Top comments (0)