Building Modern AI Applications: A Full-Stack Approach
In today's rapidly evolving web landscape, integrating intelligent APIs into robust, scalable applications is paramount. This guide demonstrates how to combine the power of Express.js for a backend, the cutting-edge Perplexity API for AI capabilities, TypeScript for type safety, a modern frontend framework (we'll use React), and finally, deploy it all effortlessly on Render. By the end, you'll have a fully functional, AI-powered application structure ready for production.
Architectural Overview
Our application will follow a classic client-server architecture:
- Frontend: A React application (built with Vite) written in TypeScript, responsible for user interaction and displaying AI-generated content.
- Backend: An Express.js server written in TypeScript, acting as an intermediary to the Perplexity API, handling API key security, and business logic.
- AI Engine: The Perplexity API, providing advanced conversational AI or information retrieval capabilities.
- Deployment: Render, offering a seamless platform-as-a-service (PaaS) solution for both backend and frontend.
Let's dive into the implementation.
Backend: Express.js with TypeScript and Perplexity API
First, set up your Express backend. Create a new directory, e.g., server, and initialize a TypeScript project.
mkdir server
cd server
npm init -y
npm install express dotenv @types/express @types/node ts-node typescript
npx tsc --init
For the Perplexity API, you'll need an API client. We'll use axios for simplicity.
npm install axios
Create a .env file in your server directory to store your PERPLEXITY_API_KEY.
PERPLEXITY_API_KEY="YOUR_PERPLEXITY_API_KEY"
Now, let's write src/server.ts:
import express, { Request, Response } from 'express';
import dotenv from 'dotenv';
import axios from 'axios';
import cors from 'cors';
dotenv.config();
const app = express();
const port = process.env.PORT || 5000;
// Configure CORS for your frontend origin
app.use(cors({ origin: 'http://localhost:5173' })); // Adjust for your frontend's dev port
app.use(express.json());
interface PerplexityRequestBody {
model: string;
messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>;
}
interface PerplexityResponse {
choices: Array<{ message: { content: string } }>;
}
app.post('/api/ask-ai', async (req: Request, res: Response) => {
const { prompt } = req.body;
if (!prompt) {
return res.status(400).json({ error: 'Prompt is required.' });
}
try {
const perplexityApiKey = process.env.PERPLEXITY_API_KEY;
if (!perplexityApiKey) {
throw new Error('Perplexity API key not configured.');
}
const requestBody: PerplexityRequestBody = {
model: "sonar-small-online", // Or other Perplexity models like 'sonar-medium-online'
messages: [
{ role: "system", content: "You are a helpful assistant providing concise answers." },
{ role: "user", content: prompt }
]
};
const response = await axios.post<PerplexityResponse>(
'https://api.perplexity.ai/chat/completions',
requestBody,
{
headers: {
'Authorization': `Bearer ${perplexityApiKey}`,
'Content-Type': 'application/json',
},
}
);
const aiResponse = response.data.choices[0]?.message.content || 'No response from AI.';
res.json({ response: aiResponse });
} catch (error: any) {
console.error('Error calling Perplexity API:', error.response?.data || error.message);
res.status(500).json({ error: 'Failed to get AI response.' });
}
});
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
Update tsconfig.json to output to dist and package.json with a build script:
// package.json
{
"name": "server",
"version": "1.0.0",
"main": "dist/server.js",
"scripts": {
"build": "npx tsc",
"start": "node dist/server.js",
"dev": "ts-node src/server.ts"
},
// ... rest of package.json
}
Frontend: React with TypeScript
Next, create your React frontend. We'll use Vite for a quick setup. Navigate back to your project root and create a client directory.
cd ..
npm create vite client -- --template react-ts
cd client
npm install
Modify client/src/App.tsx to call your backend.
import React, { useState } from 'react';
function App() {
const [prompt, setPrompt] = useState<string>('');
const [response, setResponse] = useState<string>('');
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
setResponse('');
try {
const res = await fetch('http://localhost:5000/api/ask-ai', { // Adjust if your backend runs on a different port/host
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ prompt }),
});
if (!res.ok) {
const errorData = await res.json();
throw new Error(errorData.error || 'Failed to fetch AI response.');
}
const data = await res.json();
setResponse(data.response);
} catch (err: any) {
setError(err.message || 'An unexpected error occurred.');
} finally {
setLoading(false);
}
};
return (
<div style={{ padding: '20px', maxWidth: '600px', margin: 'auto' }}>
<h1>AI Query App</h1>
<form onSubmit={handleSubmit}>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
placeholder="Ask the AI anything..."
rows={5}
style={{ width: '100%', marginBottom: '10px' }}
disabled={loading}
/>
<button type="submit" disabled={loading}>
{loading ? 'Thinking...' : 'Get AI Response'}
</button>
</form>
{error && <p style={{ color: 'red' }}>Error: {error}</p>}
{response && (
<div style={{ marginTop: '20px', border: '1px solid #ccc', padding: '15px' }}>
<h3>AI Response:</h3>
<p>{response}</p>
</div>
)}
</div>
);
}
export default App;
Deployment with Render
Render makes deploying full-stack applications incredibly straightforward. We'll deploy the backend as a Web Service and the frontend as a Static Site.
1. Backend Deployment
- Connect to Git: Link your GitHub/GitLab repository containing your
serverdirectory to Render. - Create a New Web Service: Choose "Web Service" for your backend.
- Configuration:
- Name:
perplexity-api-backend - Root Directory:
/server(if your Express app is in aserversubfolder) - Build Command:
npm install && npm run build - Start Command:
npm start - Environment Variables: Add
PERPLEXITY_API_KEYwith your actual key.
- Name:
Render will detect your package.json, install dependencies, build your TypeScript code, and start the server. Ensure your server.ts listens on process.env.PORT.
2. Frontend Deployment
- Connect to Git: Use the same repository.
- Create a New Static Site: Choose "Static Site" for your frontend.
- Configuration:
- Name:
perplexity-frontend - Root Directory:
/client - Build Command:
npm install && npm run build - Publish Directory:
dist(Vite's default build output)
- Name:
Once deployed, Render will provide a public URL for your frontend. Update the fetch call in your client/src/App.tsx to use your deployed backend's URL instead of http://localhost:5000. Remember to adjust your backend's CORS policy (app.use(cors(...))) to include your deployed frontend's URL.
Best Practices and Beyond
- Error Handling: Implement more robust error handling and logging, especially for production environments.
- Security: Sanitize user inputs to prevent injection attacks. Always keep API keys secure using environment variables.
- Scalability: For high-traffic applications, consider caching AI responses or optimizing API calls.
- TypeScript Advantages: Leverage TypeScript's type inference and explicit types for better code maintainability and fewer runtime errors.
- CI/CD: Integrate Render with your Git repository for automatic deployments on push, streamlining your development workflow.
Conclusion
By integrating Express.js, the Perplexity API, TypeScript, and Render, you've built a powerful, type-safe, and easily deployable AI-powered application. This stack provides a robust foundation for developing cutting-edge web experiences, allowing you to focus on innovation rather than infrastructure. Happy coding!