This article was written by Carlos Barboza.
I'm happy to welcome you to this tutorial! This demonstration provides a fully functional example of integrating FastAPI, LangChain, and MongoDB, and I'm eager to share it with all technology enthusiasts.
The value of this guide is that it provides a working example—you can copy and paste the code, and it will run seamlessly without any further configuration. It will also give you a clear, practical overview of the kinds of applications you can build using these powerful tools.
I am developing a suggestion application designed to assist customers globally in generating new ideas. This involves providing personalized recommendations and checking for existing companies that offer comparable services.
Prerequisites
- Python, virtualenv basics
- Basic understanding of REST APIs
- MongoDB basics (collections, documents)
- Accounts/keys
- MongoDB Atlas or local MongoDB
- LLM provider API key
Project Architecture

FastAPI is a Python framework for building APIs quickly, efficiently, and with very little code.
It’s known for being fast (as the name implies), easy to use, and perfect for modern applications like AI, microservices, and backend APIs. In this example, it will handle the API queries for the backend.
MongoDB is a NoSQL, document-based database that stores data in flexible JSON-like documents instead of rigid tables like SQL. This makes it fast to develop, easy to scale, and great for evolving data structures. For our case, it will store all the information.
LangChain is a framework for building applications powered by LLMs (like GPT, LLaMA, Ollama, and Mistral) that lets you connect models with tools, memory, external data, and multi-step reasoning. It provides the necessary tools in Python for utilizing AI models.
Project Setup
MongoDB Atlas
The simplest way to obtain a MongoDB instance for this guide is to register on the official site. This will provide you with an M0 instance, which is entirely sufficient for executing this guide.
Docker
Should you not have Docker installed, the official link provides installation instructions based on your operating system.
The docker-compose file is configured to set up a MongoDB container. This container uses the official version 7.0, is named mongodb, and uses the credentials root as the username and example as the password. Furthermore, a volume is defined to ensure data persistence across container reloads.
version: "3.3"
services:
mongo:
image: mongo:7.0
container_name: mongodb
restart: unless-stopped
ports:
- "27017:27017"
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: example
volumes:
# Named volume for persistent data
- mongo_data:/data/db
volumes:
mongo_data:
Execute the following commands to execute the docker:
docker compose up -d
Finally, you can connect to the MongoDB instance using the connection string. In your case, it’s mongodb://root:example@localhost:27017.
LLMs
Large language models (LLMs) are AI models trained on massive amounts of text that can understand language, generate text, write code, answer questions, and hold conversations like a human.
They don’t think—they predict the next word intelligently based on patterns they've learned. For our case, we are going to use Ollama so you can execute the example on your local machine.
Alternatively, you may choose to use a different LLM, such as OpenAI, though this may incur additional costs.
Ollama installation
Depending on your operating system, you must follow the official instructions. At the end of the day, you should be able to execute Ollama commands in your terminal.
ollama list # Shows all the models available in your terminal
ollama pull llama3.2 # Download the llama3.2 model
Development
Requirements.txt
These are all the libraries needed for the project:
aiohappyeyeballs==2.6.1
aiohttp==3.13.2
aiosignal==1.4.0
annotated-doc==0.0.4
annotated-types==0.7.0
anyio==4.12.0
attrs==25.4.0
bcrypt==3.2.2
certifi==2025.11.12
cffi==2.0.0
charset-normalizer==3.4.4
click==8.3.1
colorama==0.4.6
cryptography==46.0.3
dataclasses-json==0.6.7
distro==1.9.0
dnspython==2.8.0
ecdsa==0.19.1
email-validator==2.3.0
fastapi==0.123.0
frozenlist==1.8.0
greenlet==3.2.4
h11==0.16.0
httpcore==1.0.9
httpx==0.28.1
httpx-sse==0.4.3
idna==3.11
jiter==0.12.0
jsonpatch==1.33
jsonpointer==3.0.0
langchain==1.1.0
langchain-classic==1.0.0
langchain-community==0.4.1
langchain-core==1.1.0
langchain-openai==1.1.0
langchain-text-splitters==1.0.0
langgraph==1.0.4
langgraph-checkpoint==3.0.1
langgraph-prebuilt==1.0.5
langgraph-sdk==0.2.10
langsmith==0.4.49
marshmallow==3.26.1
motor==3.7.1
multidict==6.7.0
mypy_extensions==1.1.0
numpy==2.3.5
openai==2.8.1
orjson==3.11.4
ormsgpack==1.12.0
packaging==25.0
passlib==1.7.4
propcache==0.4.1
pyasn1==0.6.1
pycparser==2.23
pydantic==2.12.5
pydantic-settings==2.12.0
pydantic_core==2.41.5
pymongo==4.15.4
python-dotenv==1.2.1
python-jose==3.5.0
python-multipart==0.0.20
PyYAML==6.0.3
regex==2025.11.3
requests==2.32.5
requests-toolbelt==1.0.0
rsa==4.9.1
six==1.17.0
sniffio==1.3.1
SQLAlchemy==2.0.44
starlette==0.50.0
tenacity==9.1.2
tiktoken==0.12.0
tqdm==4.67.1
typing-inspect==0.9.0
typing-inspection==0.4.2
typing_extensions==4.15.0
urllib3==2.5.0
uvicorn==0.38.0
xxhash==3.6.0
yarl==1.22.0
zstandard==0.25.0
Environment file
It’s recommended to have an .env file with sensitive information like the MongoDB URI, JWT Secret Key, and other settings. This file should be excluded from the git.ignore and must be configured on the main location.
MONGO_URI=mongodb+srv://admin:admin@atlascluster.f3spasq.mongodb.net/
MONGO_DB_NAME=fastapi_demo
JWT_SECRET_KEY=super-secret-key
JWT_ALGORITHM=HS256
ACCESS_TOKEN_EXPIRE_MINUTES=60
OLLAMA_HOST=http://host.docker.internal:11434 # Only if fastAPI is running with docker
.gitignore
# ---------------------
# Environment variables
# ---------------------
.env
.env.local
.env.*.local
# ---------------------
# Python
# ---------------------
__pycache__/
*.pyc
*.pyo
*.pyd
*.pdb
*.pkl
*.db
# Virtual environments
venv/
.venv/
# Byte-compiled
*.py[cod]
*$py.class
# ---------------------
# FastAPI / Uvicorn logs
# ---------------------
*.log
# ---------------------
# IDE & editor files
# ---------------------
.vscode/
.idea/
*.swp
# ---------------------
# OS files
# ---------------------
.DS_Store
Thumbs.db
# ---------------------
# Docker
# ---------------------
/data/
docker-data/
*.pid
# ---------------------
# Optional cache folders
# ---------------------
cache/
logs/
Schema folder
chat.py
from pydantic import BaseModel
class ChatRequest(BaseModel):
question: str
class ChatResponse(BaseModel):
answer: str
used_context: bool
item.py
from pydantic import BaseModel
class Item(BaseModel):
id: str
name: str
price: float
user.py
from pydantic import BaseModel, EmailStr
class UserCreate(BaseModel):
email: EmailStr
password: str
class UserPublic(BaseModel):
id: str
email: EmailStr
class Token(BaseModel):
access_token: str
token_type: str = "bearer"
class UserInDB(BaseModel):
id: str
email: EmailStr
hashed_password: str
notes.py
from pydantic import BaseModel
class NotesCreate(BaseModel):
text: str
class NotesInDB(BaseModel):
id: str
user_id: str
text: str
Core folder
config.py
import os
MONGO_URI = os.getenv("MONGO_URI", "mongodb+srv://admin:admin@atlascluster.f3spasq.mongodb.net/")
MONGO_DB_NAME = os.getenv("MONGO_DB_NAME", "fastapi_demo")
JWT_SECRET_KEY = os.getenv("JWT_SECRET_KEY", "change-me")
JWT_ALGORITHM = os.getenv("JWT_ALGORITHM", "HS256")
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "60"))
llm.py
import os
from langchain_community.chat_models import ChatOllama
OLLAMA_HOST = os.getenv("OLLAMA_HOST", "http://localhost:11434")
llm = ChatOllama(
model="llama3.2"
,base_url=OLLAMA_HOST,
)
security.py
from datetime import datetime, timedelta, timezone
from typing import Optional
from fastapi.security import OAuth2PasswordBearer
from jose import jwt
from passlib.context import CryptContext
from app.core.config import JWT_SECRET_KEY, JWT_ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/login")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def create_access_token(
data: dict,
expires_delta: Optional[timedelta] = None,
) -> str:
to_encode = data.copy()
expire = datetime.now(timezone.utc) + (
expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, JWT_SECRET_KEY, algorithm=JWT_ALGORITHM)
return encoded_jwt
db folder
from motor.motor_asyncio import AsyncIOMotorClient
from app.core import config as core_config
client = AsyncIOMotorClient(core_config.MONGO_URI)
db = client[core_config.MONGO_DB_NAME]
users_collection = db["users"]
notes_collection = db["notes"]
items_collection = db["items"]
depsFolder
auth.py
from typing import Optional, Annotated
from fastapi import Depends, HTTPException, status
from jose import JWTError, jwt
from app.core.config import JWT_SECRET_KEY, JWT_ALGORITHM
from app.core.security import oauth2_scheme, hash_password, verify_password
from app.db.mongo import users_collection
from app.schemas.user import UserInDB, UserCreate
async def get_user_by_email(email: str) -> Optional[UserInDB]:
doc = await users_collection.find_one({"email": email})
if not doc:
return None
return UserInDB(
id=str(doc["_id"]),
email=doc["email"],
hashed_password=doc["hashed_password"],
)
async def create_user(user: UserCreate) -> UserInDB:
existing = await get_user_by_email(user.email)
if existing:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered",
)
hashed = hash_password(user.password)
res = await users_collection.insert_one(
{"email": user.email, "hashed_password": hashed}
)
return UserInDB(id=str(res.inserted_id), email=user.email, hashed_password=hashed)
async def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)]
) -> UserInDB:
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM])
email: str = payload.get("sub")
if email is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = await get_user_by_email(email)
if user is None:
raise credentials_exception
return user
Routers folder
auth.py
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from app.core.security import create_access_token, verify_password
from app.deps.auth import create_user, get_user_by_email, get_current_user
from app.schemas.user import UserCreate, UserPublic, Token, UserInDB
router = APIRouter(prefix="/auth", tags=["auth"])
@router.post("/register", response_model=UserPublic)
async def register(user: UserCreate):
new_user = await create_user(user)
return UserPublic(id=new_user.id, email=new_user.email)
@router.post("/login", response_model=Token)
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
# I am using email as the username field in the OAuth2 form.
user = await get_user_by_email(form_data.username)
if not user or not verify_password(form_data.password, user.hashed_password):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect email or password",
)
access_token = create_access_token(data={"sub": user.email})
return Token(access_token=access_token)
@router.get("/me", response_model=UserPublic)
async def get_me(
current_user: Annotated[UserInDB, Depends(get_current_user)]
):
return UserPublic(id=current_user.id, email=current_user.email)
chat.py
from typing import Annotated
from fastapi import APIRouter, Depends
from langchain_core.messages import HumanMessage, SystemMessage
from app.core.llm import llm
from app.db.mongo import notes_collection
from app.deps.auth import get_current_user
from app.schemas.user import UserInDB
from app.schemas.chat import ChatRequest, ChatResponse
router = APIRouter(prefix="/chat", tags=["chat"])
@router.post("/", response_model=ChatResponse)
async def chat_with_ai(
body: ChatRequest,
current_user: Annotated[UserInDB, Depends(get_current_user)],
# Uses LangChain + OpenAI + some context from MongoDB.
):
print(current_user)
notes_cursor = (
notes_collection.find({"user_id": current_user.id})
.sort("_id", -1)
.limit(3)
)
notes = await notes_cursor.to_list(length=3)
context_text = "
".join(
note.get("text", "") for note in notes if note.get("text")
)
used_context = bool(context_text)
system_prompt = (
"You are an assistant helping the user with their notes."
" "
"Use the following context if relevant; if not, just answer normally.
"
f"Context:
{context_text or '(no context available)'}"
)
messages = [
SystemMessage(content=system_prompt),
HumanMessage(content=body.question),
]
response = await llm.ainvoke(messages)
return ChatResponse(answer=response.content, used_context=used_context)
items.py
from typing import List, Annotated
from fastapi import APIRouter, Depends
from app.db.mongo import items_collection
from app.schemas.item import Item
from app.schemas.user import UserInDB
from app.deps.auth import get_current_user
router = APIRouter(prefix="/items", tags=["items"])
@router.get("/", response_model=List[Item])
async def list_items(
current_user: Annotated[UserInDB, Depends(get_current_user)]
):
# Example async Mongo query using Motor.
# Demo: seed items if collection is empty
count = await items_collection.count_documents({})
if count == 0:
await items_collection.insert_many(
[
{"name": "Book", "price": 10.5},
{"name": "Laptop", "price": 999.99},
]
)
docs = await items_collection.find().to_list(length=100)
return [
Item(id=str(doc["_id"]), name=doc["name"], price=float(doc["price"]))
for doc in docs
]
notes.py
from typing import Annotated
from fastapi import APIRouter, Depends
from langchain_core.messages import HumanMessage, SystemMessage
from app.core.llm import llm
from app.db.mongo import notes_collection
from app.deps.auth import get_current_user
from app.schemas.user import UserInDB
from app.schemas.chat import ChatRequest, ChatResponse
from app.schemas.notes import NotesCreate, NotesInDB
router = APIRouter(prefix="/notes", tags=["notes"])
@router.post("/", response_model=NotesInDB)
async def create_note(body: NotesCreate,current_user: Annotated[UserInDB, Depends(get_current_user)]):
print(current_user)
note = await notes_collection.insert_one({"text": body.text, "user_id": current_user.id})
return NotesInDB(id=str(note.inserted_id), text=body.text, user_id=current_user.id)
Main.py
from fastapi import FastAPI
from app.routers import auth, items, chat, notes
app = FastAPI(title="FastAPI + MongoDB + JWT + LangChain Example")
@app.get("/")
async def root():
return {"message": "FastAPI + MongoDB + JWT + LangChain example"}
# Include routers
app.include_router(auth.router)
app.include_router(items.router)
app.include_router(chat.router)
app.include_router(notes.router)
Application execution
On the main folder, you can execute the following command:
uvicorn app.main:app --reload
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
If you access the page http://127.0.0.1:8000/docs#, you can find the Swagger documentation created by default.
Users creation
You can execute the following command to create a new user:
curl -X 'POST' \
'http://127.0.0.1:8000/auth/register' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"email": "user@example.com",
"password": "string"
}'
Or if you have vscode Rest Api Extension you can execute
POST http://127.0.0.1:8000/auth/register
Accept: application/json
Content-Type: application/json
{
"email": "user@example.com",
"password": "string"
}
The above command will create a new user with hashed password:
Users authentication
In order to authenticate the user and retrieve the bearer token, please execute the following command:
curl -X 'POST' \
'http://127.0.0.1:8000/auth/login' \
-H 'accept: application/json' \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=password&username=user%40example.com&password=string&scope=&client_id=string&client_secret=string'
Rest API extension
POST http://127.0.0.1:8000/auth/login
Content-Type: application/x-www-form-urlencoded
accept: application/json
grant_type=password&username=test@example.com&password=123&scope=&client_id=string&client_secret=string
After a successful authentication, you will receive the following output. The token is important to authenticate on the remaining routes.
HTTP/1.1 200 OK
date: Tue, 02 Dec 2025 17:40:11 GMT
server: uvicorn
content-length: 180
content-type: application/json
Connection: close
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiZXhwIjoxNzY0NzAwODExfQ.riXj1vvLZSyzyU4T7kG3ygYaUU8sgvgCjK_Os_uggvo",
"token_type": "bearer"
}
Idea creation
The main purpose of the application is to make suggestions based on ideas that the end users have. The following request will create those notes.
POST http://127.0.0.1:8000/notes/
Content-Type: application/json
Authorization: Bearer {{token}}
{
"text": "testing123"
}
Chat interaction
The application now offers advice based on the notes stored by users in the database. To enable interaction with the LLM, we define the chat's behavior in the chat.py file. This definition is an example of prompt engineering, and this is the section where you can modify the prompt's language and content, if needed.
system_prompt = (
"You are an assistant helping the user with their notes."
" "
"Use the following context if relevant; if not, just answer normally.
"
f"Context:
{context_text or '(no context available)'}"
)
Execution example:
POST http://127.0.0.1:8000/chat/
Content-Type: application/json
Authorization: Bearer {{token}}
{
"question": "What i need to do"
}
Execution output:
{
"answer": "To help you achieve your goals of saving more money and finishing your project, here are some actionable steps:
**Saving More Money:**
1. **Track your expenses**: Write down every single transaction, no matter how small, for a week or two to understand where your money is going.
2. **Create a budget**: Based on your income and expenses, allocate a specific amount for savings each month.
3. **Automate your savings**: Set up an automatic transfer from your checking account to your savings account.
4. **Cut back on unnecessary expenses**: Identify areas where you can cut back on unnecessary spending, such as dining out or subscription services.
5. **Consider a savings challenge**: Try a savings challenge like the \"52-week savings challenge\" where you save an amount equal to the number of the week (e.g., Week 1: Save $1, Week 2: Save $2 etc.).
**Finishing Your Project:**
1. **Break down your project into smaller tasks**: Divide your project into manageable tasks to make it feel less overwhelming.
2. **Create a schedule**: Set a specific deadline for each task and stick to it.
3. **Prioritize your tasks**: Focus on the most critical tasks first, and then move on to less important ones.
4. **Remove distractions**: Identify potential distractions (e.g., social media, email, phone notifications) and eliminate them while you work.
5. **Take regular breaks**: Take short breaks to recharge and maintain productivity.
Which of these steps do you want to start with?",
"used_context": true
}
Docker (optional)
Our application is currently functional. However, to execute it in a different environment, we must install the necessary dependencies. Docker can facilitate and streamline this deployment process.
Create Dockerfile and paste the following code:
# Dockerfile
FROM python:3.11-slim
# Set work directory inside the container
WORKDIR /app
# Install system dependencies (optional but useful for many libs)
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Copy only requirements first (for better Docker cache)
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of your project
COPY . .
# Expose the FastAPI port
EXPOSE 8000
# Default command: run uvicorn
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Create the docker-compose.yml:
version: "3.8"
services:
fastapi:
build: .
container_name: fastapi-langchain
ports:
- "8000:8000"
env_file:
- .env # optional: if you have one
extra_hosts:
- "host.docker.internal:host-gateway"
restart: unless-stopped
Now, we can build and execute our docker:
docker-compose build
docker-compose up -d
Conclusion
This tutorial has provided a complete, practical demonstration of building a modern, data-driven application by seamlessly integrating FastAPI, LangChain, and MongoDB.
We have successfully covered:
- FastAPI for creating a robust, fast, and documented REST API, including user registration and JWT-based authentication.
- MongoDB (using Motor) for asynchronous and scalable document storage, handling user data and application-specific notes.
- LangChain (with Ollama) for enabling powerful AI capabilities, using the stored user notes as dynamic context to generate personalized, helpful suggestions and answers.
The example application, an idea generation assistant, serves as a clear blueprint for how to leverage this stack for complex projects requiring data persistence, secure user access, and cutting-edge large language model integration. By using a local LLM via Ollama, we also ensured the entire stack is executable on a local machine, making it accessible for rapid prototyping and development.
This integration strategy—combining the speed of FastAPI, the flexibility of MongoDB, and the intelligence of LangChain—is a highly effective pattern for developing the next generation of intelligent web applications. We hope this guide serves as a solid foundation for your future AI-powered projects.





