Build Backend With Express.js: A Step-by-Step Guide
Hey guys! Let's dive into building a robust backend server using Express.js. This guide will walk you through setting up your server, integrating with APIs, and handling client requests. We'll cover everything from initializing Express to streaming responses from the Gemini API. So, buckle up and let's get started!
Objective
Our main goal here is to create a core backend server in server/server.js
using Express.js. This involves several key steps, including setting up the server, serving static files, caching an OpenAPI spec, creating a chat endpoint, integrating with a Large Language Model (LLM), and streaming responses back to the client.
1. Initialize Express.js
First, we need to set up a basic Express server. Express.js is a minimal and flexible Node.js web application framework that provides a robust set of features for web and mobile applications. Let's start by initializing our Express server and setting it to listen on port 3000. This is the foundation upon which we'll build the rest of our backend.
const express = require('express');
const app = express();
const port = 3000;
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
This code snippet is the starting point. We require the express
module, create an instance of an Express application, and then tell the app to listen for connections on port 3000. The callback function logs a message to the console, letting us know the server is up and running. It's simple, but crucial.
2. Static File Serving
Next, we'll configure the server to serve static files. Static files are assets like HTML, CSS, JavaScript, and images that don't change dynamically. These files are typically stored in a directory, and we want our server to be able to serve them to clients. This is essential for any web application that has a front-end component.
const path = require('path');
app.use(express.static(path.join(__dirname, '../public')));
Here, we use the express.static
middleware to serve files from the ../public
directory. The path.join
function ensures that we're constructing the correct path to the directory, regardless of the operating system. This means that when a client requests a file like index.html
, the server will look for it in the ../public
directory and serve it if found. This is a fundamental part of building a web application, allowing us to serve our front-end code.
3. OpenAPI Caching
Now, let's use 'axios' to fetch the OpenAPI spec from 'https://api.sandbox.travelpay.com.au/v2.0/help' on server startup and store it in a local variable. OpenAPI specifications are crucial for understanding the structure and capabilities of an API. Caching this spec locally can significantly improve performance and reduce the load on the external API.
const axios = require('axios');
let openAPISpec;
async function fetchOpenAPISpec() {
try {
const response = await axios.get('https://api.sandbox.travelpay.com.au/v2.0/help');
openAPISpec = response.data;
console.log('OpenAPI spec fetched and cached.');
} catch (error) {
console.error('Error fetching OpenAPI spec:', error);
}
}
fetchOpenAPISpec(); // Call this function on server startup
In this snippet, we use the axios
library to make an HTTP GET request to the provided URL. The response, which contains the OpenAPI spec, is then stored in the openAPISpec
variable. We also include error handling to catch any issues during the fetch process. By fetching and caching the spec on server startup, we ensure that it's readily available when we need it, which is especially important for integrating with LLMs later on. This is a key optimization step that can improve the overall responsiveness of our application.
4. Create Chat Endpoint
It's time to implement a POST endpoint at '/api/chat' that accepts { question, apiKey }
. This endpoint will be the entry point for chat requests from the client. It needs to be able to receive the user's question and API key, which we'll use to interact with the Gemini API. Creating this endpoint is a crucial step in building our chat functionality.
app.use(express.json()); // Middleware to parse JSON bodies
app.post('/api/chat', async (req, res) => {
const { question, apiKey } = req.body;
if (!question || !apiKey) {
return res.status(400).json({ error: 'Question and API key are required.' });
}
// Process the question and interact with the LLM here
});
Here, we first use the express.json()
middleware to parse JSON request bodies. This allows us to easily access the question
and apiKey
from the request body. We then define a POST route for /api/chat
. Inside the route handler, we extract the question
and apiKey
from the request body and perform a basic validation to ensure both are present. If either is missing, we return a 400 status code with an error message. This is an essential step in handling incoming requests and ensuring that we have the necessary information to process them. The // Process the question and interact with the LLM here
comment indicates where we'll add the logic to interact with the Gemini API in the next step.
5. LLM Integration
Now, we get to the exciting part: integrating with a Large Language Model (LLM). Specifically, we'll use the @google/generative-ai
library to call the Gemini API. Inside the /api/chat
endpoint, we'll construct a prompt that includes the cached OpenAPI spec as context and the user's question. This will allow the Gemini API to understand the context of the API and provide more relevant and accurate responses.
const { GoogleGenerativeAI } = require('@google/generative-ai');
app.post('/api/chat', async (req, res) => {
const { question, apiKey } = req.body;
if (!question || !apiKey) {
return res.status(400).json({ error: 'Question and API key are required.' });
}
const genAI = new GoogleGenerativeAI(apiKey);
const model = genAI.getGenerativeModel({ model: 'gemini-pro' });
const prompt = `You are an API expert. Use the following OpenAPI spec as context:
${JSON.stringify(openAPISpec, null, 2)}
User Question: ${question}`;
try {
const result = await model.generateContent(prompt);
const response = await result.response;
const text = response.text();
res.json({ response: text });
} catch (error) {
console.error('Error generating content:', error);
res.status(500).json({ error: 'Failed to generate response.' });
}
});
In this enhanced snippet, we first require the GoogleGenerativeAI
class from the @google/generative-ai
library. We then create a new instance of GoogleGenerativeAI
with the provided API key and get the gemini-pro
model. The key to effective LLM integration is crafting a clear and informative prompt. Here, we create a prompt that includes the cached OpenAPI spec and the user's question. We instruct the LLM to act as an API expert and use the OpenAPI spec as context. This helps the LLM understand the API's capabilities and how to answer the user's question. We then use the model.generateContent
method to generate a response from the Gemini API. The response is extracted and sent back to the client as a JSON object. We also include error handling to catch any issues during the content generation process. This step is where we bring the power of LLMs into our backend, enabling us to provide intelligent and context-aware responses to user queries. This is a game-changer for creating conversational interfaces and intelligent applications.
6. Stream Response
Finally, let's ensure the response from the Gemini API is streamed back to the client. Streaming responses can significantly improve the user experience, especially for long-running operations or large responses. Instead of waiting for the entire response to be generated, we can send chunks of data as they become available. This makes the application feel more responsive and interactive.
const { GoogleGenerativeAI } = require('@google/generative-ai');
app.post('/api/chat', async (req, res) => {
const { question, apiKey } = req.body;
if (!question || !apiKey) {
return res.status(400).json({ error: 'Question and API key are required.' });
}
const genAI = new GoogleGenerativeAI(apiKey);
const model = genAI.getGenerativeModel({ model: 'gemini-pro' });
const prompt = `You are an API expert. Use the following OpenAPI spec as context:
${JSON.stringify(openAPISpec, null, 2)}
User Question: ${question}`;
try {
const result = await model.generateContentStream(prompt);
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
for await (const chunk of result.stream) {
const text = chunk.text();
res.write(text);
}
res.end();
} catch (error) {
console.error('Error generating content:', error);
res.status(500).json({ error: 'Failed to generate response.' });
}
});
In this updated snippet, we use the model.generateContentStream
method instead of model.generateContent
. This method returns an asynchronous iterable that yields chunks of the response as they become available. We set the Content-Type
header to text/plain; charset=utf-8
to indicate that we're streaming plain text. We then iterate over the chunks using a for await...of
loop. For each chunk, we extract the text and write it to the response stream using res.write
. Finally, we call res.end()
to signal the end of the response. By streaming the response, we can provide a more responsive and interactive experience for the user, especially when dealing with large amounts of data or long-running processes. This is a crucial technique for building modern, performant web applications.
Conclusion
Alright, guys! We've successfully built a backend server with Express.js that integrates with the Gemini API and streams responses back to the client. We've covered a lot of ground, from initializing Express to crafting prompts for LLMs. This is a solid foundation for building more complex and intelligent applications. Keep experimenting and exploring, and you'll be amazed at what you can create!
Remember, building a backend is an iterative process. Don't be afraid to experiment, make mistakes, and learn from them. The more you practice, the better you'll become. And most importantly, have fun! Happy coding!