Neo4j and FastAPI Tutorial
FastAPI is a fantastic library for building APIs with Python. From the documentation:
FastAPI is a modern, fast (high-performance), web framework for building APIs with Python 3.6+ based on standard Python type hints.
Because both FastAPI and Neontology utilise Pydantic models, we can combine them to quickly build a Python based Neo4j API.
We'll need a few things before we begin:
pip install fastapi
pip install "uvicorn[standard]"
You'll also need a live Neo4j instance to connect to.
Defining Models
We're going to build a limited API for managing teams and team members.
First we need to define models for the different types of node we want to store.
# main.py
from typing import ClassVar, Optional
from fastapi import FastAPI, HTTPException
from neontology import BaseNode, BaseRelationship, init_neontology, Neo4jConfig
class TeamNode(BaseNode):
__primaryproperty__: ClassVar[str] = "teamname"
__primarylabel__: ClassVar[str] = "Team"
teamname: str
slogan: str = "Better than the rest!"
class TeamMemberNode(BaseNode):
__primaryproperty__: ClassVar[str] = "nickname"
__primarylabel__: ClassVar[str] = "TeamMember"
nickname: str
Adding a relationship
Team members should 'belong to' a team, so lets define that relationship.
class BelongsTo(BaseRelationship):
__relationshiptype__: ClassVar[str] = "BELONGS_TO"
source: TeamMemberNode
target: TeamNode
Our first root
Now we need to initialise FastAPI and Neontology, and let's create a root route to try things:
app = FastAPI()
@app.on_event("startup")
async def startup_event():
# here we declare the neo4j connection details explicitly (this can be bad for security)
# you could instead define them as environment variables or in a .env file
NEO4J_URI="neo4j+s://<database id>.databases.neo4j.io" # neo4j Aura example
NEO4J_USERNAME="neo4j"
NEO4J_PASSWORD="<your password>"
config = Neo4jConfig(
uri=NEO4J_URI,
username=NEO4J_USERNAME,
password=NEO4J_PASSWORD
)
init_neontology(config)
@app.get("/")
def read_root():
return {"foo": "bar"}
What we have so far
With all this defined in main.py, we can check it's running with:
uvicorn main:app --reload
Now if you go to http://127.0.0.1:8000/ you should simply get {"foo":"bar"}.
But for a much more comprehensive view, you can checkout http://127.0.0.1:8000/docs or http://127.0.0.1:8000/redoc.
Adding teams
Now lets start actually interacting with our Neo4j graph.
We'll define a POST rout for adding a new team.
Because FastAPI and Neontology both use Pydantic models, we just need to give our node type as a type hint on the route so that FastAPI knows that data it receives should map to the TeamNode type.
Then we use Neontology's create() method to create that team in the database.
@app.post("/teams/")
async def create_team(team: TeamNode):
team.create()
return team
Now visit http://127.0.0.1:8000/docs again where you should see our POST entry for /teams/. Explore the information and then hit the Try it out button to post some information and create a team.
Getting teams
Here we'll add some more routes to get the teams that have been created.
Getting all teams
/teams/ will provide a list of all the created teams.
@app.get("/teams/")
async def get_teams() -> list[TeamNode]:
return TeamNode.match_nodes()
Getting a team based on its primary property
/teams/<teamname> will let us access information about a specific team.
@app.get("/teams/{pp}")
async def get_team(pp: str) -> Optional[TeamNode]:
return TeamNode.match(pp)
Getting teams based on filtering
You can also retrieve teams based on various filter criteria. The match_nodes method supports a wide range of filter options using a Django-like syntax. Here are some examples of how you can use filtering:
Basic filtering
To get all teams with a specific slogan:
TeamNode.match_nodes(filters={"slogan": "Better than the rest!"})
String-based filters
icontains: Case-insensitive containspython TeamNode.match_nodes(filters={"teamname__icontains": "team"})contains: Case-sensitive containspython TeamNode.match_nodes(filters={"teamname__contains": "Team"})iexact: Case-insensitive exact matchpython TeamNode.match_nodes(filters={"teamname__iexact": "team a"})startswith: Case-sensitive startswithpython TeamNode.match_nodes(filters={"teamname__startswith": "Tea"})istartswith: Case-insensitive startswithpython TeamNode.match_nodes(filters={"teamname__istartswith": "tea"})
Numeric filters
For numeric fields (if any were present), you could use:
gt: Greater thanlt: Less thangte: Greater than or equal tolte: Less than or equal to
Boolean filters
For boolean fields, you can simply use the field name with the desired boolean value:
TeamNode.match_nodes(filters={"is_active": True})
Null checks
To filter based on null values:
TeamNode.match_nodes(filters={"slogan__isnull": True}) # Teams with no slogan
Combining filters
You can combine multiple filters to create more complex queries:
TeamNode.match_nodes(filters={
"slogan__icontains": "better",
"teamname__startswith": "A"
})
Getting the count of teams with optional filtering
You can retrieve the total count of teams, optionally filtered based on certain criteria. This is useful for retrieving statistics or counts of teams that meet specific conditions. The get_count method is used for this purpose and supports the same Django-like filter syntax as described in the filtering section above. Here are examples of how you can use it:
To get the total count of all teams without any filters:
TeamNode.get_count()
To get the count of all teams with a specific slogan:
TeamNode.get_count(filters={"slogan": "Better than the rest!"})
For other filter criteria, refer to the examples provided in the filtering section. This flexibility allows you to efficiently count teams based on complex criteria, making it useful for analytics and reporting purposes.
Pagination
You can also paginate results using limit and skip parameters:
TeamNode.match_nodes(limit=10, skip=20) # Get 10 teams, skipping the first 20
These filtering capabilities provide a powerful way to query your data with flexibility and precision.
You can now browse the API to create teams and get info about them.
Finishing Up
We can then add some more routes which will let us create team members and assign them to teams.
In full, this becomes:
# main.py
from typing import ClassVar, Optional
from fastapi import FastAPI, HTTPException
from neontology import BaseNode, BaseRelationship, init_neontology
class TeamNode(BaseNode):
__primaryproperty__: ClassVar[str] = "teamname"
__primarylabel__: ClassVar[str] = "Team"
teamname: str
slogan: str = "Better than the rest!"
class TeamMemberNode(BaseNode):
__primaryproperty__: ClassVar[str] = "nickname"
__primarylabel__: ClassVar[str] = "TeamMember"
nickname: str
class BelongsTo(BaseRelationship):
__relationshiptype__: ClassVar[str] = "BELONGS_TO"
source: TeamMemberNode
target: TeamNode
app = FastAPI()
@app.on_event("startup")
async def startup_event():
# make sure you've set NEO4J_URI, NEO4J_USERNAME and NEO4J_PASSWORD environment variables
# they could be defined in a .env file
init_neontology()
@app.get("/")
def read_root():
return {"foo": "bar"}
@app.post("/teams/")
async def create_team(team: TeamNode):
team.create()
return team
@app.get("/teams/")
async def get_teams() -> list[TeamNode]:
return TeamNode.match_nodes()
@app.get("/teams/{pp}")
async def get_team(pp: str) -> Optional[TeamNode]:
return TeamNode.match(pp)
@app.post("/team-members/")
async def create_team_member(member: TeamMemberNode, team_name: str):
team = TeamNode.match(team_name)
if team is None:
raise HTTPException(status_code=404, detail="Team doesn't exist")
member.create()
rel = BelongsTo(source=member, target=team)
rel.merge()
return member
@app.get("/team-members/")
async def get_team_members() -> list[TeamMemberNode]:
return TeamMemberNode.match_nodes()
@app.get("/team-members/{pp}")
async def get_team_member(pp: str) -> Optional[TeamMemberNode]:
return TeamMemberNode.match(pp)