AI applications are becoming much more interesting than simply sending a prompt to an LLM.
One of the most useful patterns for building production-style AI applications is RAG — Retrieval-Augmented Generation.
In this article, let's break down a sample full-stack RAG application and understand what happens from the moment a user enters a question until the AI generates the final response.
We will use a hypothetical application called:
DevDocs AI — An AI assistant that answers questions using a private collection of developer documentation.
What Are We Building?
Imagine that we have a website where developers can upload documentation, guides, PDFs, or technical notes.
A user can then ask:
"How do I configure authentication in this project?"
Instead of asking the LLM to answer entirely from its existing knowledge, our application retrieves relevant information from our own document collection and gives that information to the LLM as context.
The high-level architecture looks like this:
┌──────────────────┐
│ React UI │
└────────┬─────────┘
│
│ HTTP Request
▼
┌──────────────────┐
│ Node.js / │
│ Express API │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ RAG Service │
└────────┬─────────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Vector Search │ │ LLM │
│ MongoDB │ │ Gemini/OpenAI │
└───────────────┘ └───────────────┘
Now let's understand every step.
1. The User Asks a Question
Our React frontend contains a simple chat interface.
For example:
User:
"How can I refresh an expired authentication token?"
React sends the question to our backend:
POST /api/chat
Request:
{
"question": "How can I refresh an expired authentication token?"
}
The frontend doesn't need to know how RAG works.
Its job is simply:
Accept the user's question.
Send it to the API.
Display the response.
2. Express Receives the Request
Our Node.js backend receives the request.
A simplified controller could look like:
app.post("/api/chat", async (req, res) => {
try {
const { question } = req.body;
const answer = await ragService.ask(question);
res.json({
answer
});
} catch (error) {
res.status(500).json({
message: "Something went wrong"
});
}
});
The controller delegates the actual AI work to a separate RAG service.
This separation is important.
Instead of putting everything inside the route handler, we can maintain something like:
controllers/
services/
models/
routes/
utils/
3. Convert the Question Into an Embedding
The next step is extremely important.
Our user's question is:
How can I refresh an expired authentication token?
A vector embedding model converts this text into a numerical representation.
Conceptually:
Question
↓
Embedding Model
↓
[0.018, -0.442, 0.731, 0.092, ...]
We don't normally interpret these numbers manually.
Instead, they allow us to perform semantic similarity searches.
For example:
"refresh expired authentication token"
could be considered semantically similar to:
"renew an access token after expiration"
even though the exact words aren't identical.
4. Search the Vector Database
Now we send the question embedding to our vector database.
In this example, we can use MongoDB Atlas Vector Search.
Our document collection might contain chunks like:
Document A:
JWT authentication configuration...
Document B:
Refresh token implementation...
Document C:
User registration...
Document D:
Database connection configuration...
The vector search compares the query vector against stored document vectors.
The result might look conceptually like:
1. Refresh Token Implementation
similarity: 0.94
2. JWT Authentication
similarity: 0.89
3. Session Management
similarity: 0.82
We select the most relevant chunks.
For example:
const relevantDocuments = await vectorSearch(questionEmbedding, 5);
5. Where Did Those Document Embeddings Come From?
This is where the ingestion pipeline comes into the picture.
Before users can ask questions, our system needs to process the original documents.
The ingestion pipeline looks like this:
Document
↓
Text Extraction
↓
Cleaning
↓
Chunking
↓
Embedding Generation
↓
Vector Storage
Suppose we upload a 20-page documentation file.
We don't necessarily want to send the entire document to the LLM every time.
Instead, we split it into smaller pieces.
For example:
Document
│
├── Chunk 1
├── Chunk 2
├── Chunk 3
├── Chunk 4
├── Chunk 5
└── ...
Each chunk receives an embedding.
Chunk
↓
Embedding Model
↓
Vector
↓
MongoDB
6. Why Do We Need Chunking?
Imagine a 100-page technical document.
A user asks:
"How do I refresh an access token?"
Sending all 100 pages to the LLM would be inefficient.
Instead, our retrieval system might identify:
Chunk 17
Chunk 24
Chunk 31
as the most relevant pieces.
This gives the LLM focused information.
Chunking therefore becomes an important part of RAG quality.
7. Build the Context
Now our backend has retrieved relevant information.
For example:
Chunk 17:
Access tokens expire after 15 minutes.
Applications should use the refresh token endpoint
to obtain a new access token.
The refresh token must be validated before issuing
a new access token.
Our RAG service creates a context:
const context = documents
.map(doc => doc.content)
.join("\n\n");
Now we have:
User Question
+
Retrieved Context
8. Construct the LLM Prompt
We can now construct a prompt such as:
You are a technical documentation assistant.
Answer the user's question using the provided context.
If the answer cannot be found in the context,
say that the information is not available.
Context:
[retrieved documentation]
Question:
How can I refresh an expired authentication token?
This is the key idea behind Retrieval-Augmented Generation.
The LLM isn't being asked to answer from the question alone.
It receives relevant external information.
9. Send the Prompt to the LLM
Now our backend sends the constructed prompt to an LLM.
For example:
const response = await llm.generate({
prompt
});
The model generates something like:
When the access token expires, the application should
validate the refresh token and use the refresh token
endpoint to request a new access token.
The refresh token should be securely stored and validated
before issuing the new token.
10. Send the Answer Back to React
The backend returns:
{
"answer": "When the access token expires..."
}
React receives the response and displays it in the chat interface.
The complete journey is therefore:
User Question
↓
React
↓
Express API
↓
Question Embedding
↓
Vector Search
↓
Relevant Chunks
↓
Context Construction
↓
Prompt
↓
LLM
↓
Generated Answer
↓
Express
↓
React
That's our basic RAG pipeline.
11. Complete Architecture
Putting everything together:
USER
│
▼
┌───────────────┐
│ React Frontend│
└───────┬───────┘
│
▼
┌───────────────┐
│ Express API │
└───────┬───────┘
│
▼
┌─────────────┐
│ RAG Service │
└──────┬──────┘
│
┌─────────┴─────────┐
│ │
▼ ▼
┌──────────────┐ ┌─────────────┐
│ Embedding │ │ MongoDB │
│ Model │ │ Vector │
└──────────────┘ │ Search │
└──────┬──────┘
│
▼
Relevant Context
│
▼
┌─────────────┐
│ LLM │
└──────┬──────┘
│
▼
Final Answer
12. What About MongoDB?
MongoDB can store much more than vectors.
We could have collections such as:
users
documents
document_chunks
chat_sessions
messages
logs
A document chunk could conceptually look like:
{
"documentId": "doc_123",
"content": "Access tokens expire after...",
"embedding": [0.018, -0.442, 0.731],
"metadata": {
"source": "authentication-guide.pdf",
"page": 12
}
}
The metadata is useful because the application can later tell the user where the retrieved information came from.
13. Why RAG Instead of Fine-Tuning?
These approaches solve different problems.
RAG
Useful when:
Your knowledge changes frequently.
You have private documents.
You need source-aware answers.
You want to retrieve specific information dynamically.
Fine-Tuning
Useful when:
You want to change model behavior or style.
You have a suitable training dataset.
You need specialized response patterns.
RAG doesn't "teach" the model permanently.
Instead, it retrieves information at query time.
14. Important Production Considerations
A demo RAG application is relatively straightforward.
A production application is a different story.
We need to consider:
Authentication
Protect API endpoints using authentication and authorization.
Rate Limiting
Prevent users from abusing expensive AI endpoints.
Input Validation
Validate user requests before processing them.
Prompt Injection
Retrieved documents and user input should not automatically be treated as trusted instructions.
Logging
Track:
request ID
user ID
retrieval results
model
latency
token usage
errors
Cost Management
LLM and embedding APIs can become expensive.
Caching can reduce repeated calls.
Retrieval Quality
Poor retrieval produces poor answers.
Therefore, improving:
chunking
embedding model
metadata filtering
top-k retrieval
reranking
can significantly improve the final result.
15. A Better RAG Pipeline
Our basic pipeline can eventually evolve into:
User Query
↓
Query Validation
↓
Query Embedding
↓
Hybrid Search
↓
Metadata Filtering
↓
Vector Retrieval
↓
Reranking
↓
Context Compression
↓
Prompt Construction
↓
LLM
↓
Citation Generation
↓
Response
This is much closer to what we might build in a serious production system.
16. Suggested MERN Project Structure
A possible project structure could look like:
rag-app/
│
├── client/
│ ├── src/
│ │ ├── components/
│ │ ├── pages/
│ │ ├── services/
│ │ └── hooks/
│
├── server/
│ ├── controllers/
│ ├── routes/
│ ├── services/
│ │ ├── embeddingService.js
│ │ ├── retrievalService.js
│ │ ├── ragService.js
│ │ └── llmService.js
│ ├── models/
│ ├── middleware/
│ └── utils/
│
├── .env
├── package.json
└── README.md
The important idea is to keep the RAG components modular.
17. The Most Important Concept
The easiest way to understand RAG is:
Traditional LLM:
Question
↓
LLM
↓
Answer
RAG:
Question
↓
Search Knowledge Base
↓
Relevant Information
↓
LLM + Information
↓
Answer
That's the fundamental difference.
Final Thoughts
A RAG system isn't simply:
"Connect an LLM to a vector database."
A real RAG application is a pipeline consisting of several interconnected components:
Document ingestion
↓
Text processing
↓
Chunking
↓
Embeddings
↓
Vector storage
↓
Query processing
↓
Retrieval
↓
Context construction
↓
LLM generation
↓
Response
When this pipeline is integrated into a full-stack application, we can build systems such as:
AI documentation assistants
Internal company knowledge bots
Customer-support assistants
Research assistants
Developer copilots
Legal/document search systems
Product knowledge assistants
Educational AI applications
The interesting part isn't just calling an LLM.
The interesting engineering challenge is building the entire pipeline around it.
That's where RAG becomes a real full-stack engineering project rather than just an AI demo.
Suggested next step
A natural next project would be to actually implement this architecture using:
React + Node.js + Express + MongoDB Atlas Vector Search + an embedding model + Gemini/OpenAI API
and build it incrementally from document upload → ingestion → vector storage → semantic search → RAG chat UI.

Comments
Post a Comment
Thanks for reading! Feel free to drop a question or feedback