cd..blog

Full-Stack AI: Express, Perplexity, TypeScript, & Render

const published = "Oct 13, 2025, 05:36 AM";const readTime = 7 min;
expresstypescriptperplexityrenderfullstack
Build a robust full-stack TypeScript application integrating Express.js, the Perplexity API, and a React frontend, then deploy it seamlessly on Render.

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 server directory 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 a server subfolder)
    • Build Command: npm install && npm run build
    • Start Command: npm start
    • Environment Variables: Add PERPLEXITY_API_KEY with your actual key.

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)

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!