Skip to content
Developer Edition runs on one host and does not require a product license file. Need multi-host deployment or performance testing at scale? Explore Enterprise Edition

Build a service with the REST API

Build a movie cast service with the REST API and FastAPI. The application accepts a movie ID, calls the NebulaGraph Database Developer Edition API, and returns JSON for the movie and its actors.

The REST API is a preview feature for evaluation and testing. See REST API for the interface reference.

Before you start

  • Follow Prepare NebulaGraph Database Developer Edition to start NebulaGraph Database Developer Edition and load the movie sample dataset.
  • Install Python 3.10 or later and know Python functions, dictionaries, and exception handling.
  • Have a NebulaGraph Database Developer Edition username and password. The application computer must be able to reach the NebulaGraph Database Developer Edition HTTP port.

The terminal commands below are for macOS and Linux. Run them on the computer running the application, which can be the development machine running Docker or another computer that can reach NebulaGraph Database Developer Edition.

1. Create the project and install dependencies

mkdir movie-rest-app
cd movie-rest-app
python3 -m venv .venv
source .venv/bin/activate
python -m pip install httpx==0.28.1 fastapi==0.142.1 uvicorn==0.54.0

HTTPX sends HTTP requests to NebulaGraph Database Developer Edition, FastAPI provides the application HTTP endpoint, and Uvicorn runs the application.

2. Configure the NebulaGraph Database Developer Edition connection

Choose the NebulaGraph Database Developer Edition address for your application computer:

Application location NebulaGraph Database Developer Edition address
On the same development machine as Docker http://127.0.0.1:19669
On another computer http://<development-machine-ip>:19669

Replace <development-machine-ip> with the IP address of the machine running Docker. If you changed the 19669:19669 mapping, use the development machine port to the left of the colon.

Set these environment variables in the terminal used to create the project. This example assumes that the application and Docker run on the same machine:

export NEBULA_HTTP_URL='http://127.0.0.1:19669'
export NEBULA_USER='root'
export NEBULA_PASSWORD="$(python -c 'import getpass; print(getpass.getpass("Database password: "))')"

At the Database password: prompt, enter NebulaGraph Database Developer Edition's current, actual password. Enter a single $ for each character escaped as $$ in the Compose file. Use this terminal for the startup command below.

Set NEBULA_HTTP_URL to the root URL of the NebulaGraph Database Developer Edition HTTP API, without /gql or /ui/console. The supplied configuration uses HTTP.

3. Save the application code

Download movie_rest.py and save it in movie-rest-app.

The example queries /default_schema/movie. If your data is in another Schema, update the graph path in both queries in the file.

View the complete code
"""Run with: python -m uvicorn movie_rest:app --host 127.0.0.1 --port 8100."""

import logging
import os
from typing import Annotated

import httpx
from fastapi import FastAPI, HTTPException, Path

app = FastAPI(title="Movie cast service (REST API)")
logger = logging.getLogger(__name__)
base_url = os.environ.get("NEBULA_HTTP_URL", "http://127.0.0.1:19669").rstrip("/")
username = os.environ.get("NEBULA_USER", "root")
password = os.environ["NEBULA_PASSWORD"]


class DatabaseError(Exception):
    pass


def post(client, path, body=None):
    response = client.post(path, json=body)
    response.raise_for_status()
    payload = response.json()
    if not isinstance(payload, dict) or payload.get("code") != "00000":
        raise DatabaseError("Database returned an unsuccessful response")
    return payload.get("data", {})


def query_rows(client, statement):
    data = post(client, "/gql", {"gql": statement})
    if not isinstance(data, dict):
        raise DatabaseError("Missing query data")
    rows = data.get("tables")
    if not isinstance(rows, list) or not all(isinstance(row, dict) for row in rows):
        raise DatabaseError("Invalid query result table")
    return rows


@app.get("/movies/{movie_id}/actors")
def get_actors(movie_id: Annotated[int, Path(ge=1, le=9223372036854775807)]):
    movie_query = (
        "USE /default_schema/movie MATCH (m@Movie) "
        f"WHERE m.id = {movie_id} RETURN m.name AS movie"
    )
    actors_query = (
        "USE /default_schema/movie MATCH (a@Actor)-[:Act]->(m@Movie) "
        f"WHERE m.id = {movie_id} "
        "RETURN a.id AS actor_id, a.name AS actor ORDER BY actor_id"
    )

    try:
        with httpx.Client(base_url=base_url, timeout=10.0) as client:
            auth = post(client, "/auth", {"username": username, "password": password})
            token = auth.get("token") if isinstance(auth, dict) else None
            if not isinstance(token, str) or not token:
                raise DatabaseError("Missing authentication token")
            client.headers["Authorization"] = f"Bearer {token}"
            try:
                movies = query_rows(client, movie_query)
                actors = query_rows(client, actors_query) if movies else []
            finally:
                # Release this request's database session even if a query fails.
                try:
                    post(client, "/signout")
                except Exception as exc:
                    logger.warning("Signout failed (%s)", type(exc).__name__)
    except Exception as exc:
        logger.warning("Database request failed (%s)", type(exc).__name__)
        raise HTTPException(status_code=502, detail="Database request failed") from exc

    if not movies:
        raise HTTPException(status_code=404, detail="Movie not found")

    return {"movie_id": movie_id, "movie": movies[0]["movie"], "actors": actors}

4. Start the application

From the project directory, with the virtual environment active, run:

python -m uvicorn movie_rest:app --host 127.0.0.1 --port 8100

If port 8100 is in use, stop the application using it or use --port 8101 and update the URLs below.

These messages indicate that the application has started:

Application startup complete.
Uvicorn running on http://127.0.0.1:8100 (Press CTRL+C to quit)

Keep this terminal running. Use a browser or another terminal to call the endpoint. The application listens on local port 8100; press Ctrl+C to stop it.

5. Call the query endpoint

On the computer running the application, use either method below to find the movie with ID 3. If the application runs remotely over SSH, first set up port forwarding as described below.

  1. Open http://127.0.0.1:8100/docs, the application's generated API documentation.
  2. Expand GET /movies/{movie_id}/actors and select Try it out.
  3. Enter 3 for movie_id, select Execute, and inspect the response.

Open another terminal on the application computer and run:

curl -sS http://127.0.0.1:8100/movies/3/actors | python3 -m json.tool --no-ensure-ascii

Both methods should return the same JSON:

{
  "movie_id": 3,
  "movie": "Shadows in Paradise",
  "actors": [
    {"actor_id": 4826, "actor": "Matti Pellonpää"},
    {"actor_id": 4828, "actor": "Sakari Kuosmanen"},
    {"actor_id": 5999, "actor": "Kati Outinen"}
  ]
}

Access a remote application over SSH

In a local terminal on the browser computer, start forwarding. Replace <ssh-user> and <remote-address> with the values used to log in to the application computer:

ssh -N -L 8100:127.0.0.1:8100 <ssh-user>@<remote-address>

For a nondefault SSH port, add -p <ssh-port>. After authentication, the forwarding terminal keeps running, usually without output.

Keep the remote application terminal and local forwarding terminal running. Open http://127.0.0.1:8100/docs in the local browser. Requests to local port 8100 are forwarded to remote application port 8100.

If the local port is occupied, change the value after -L to 8101:127.0.0.1:8100 and open http://127.0.0.1:8101/docs instead.

The application API documentation is at http://127.0.0.1:8100/docs. The NebulaGraph Database Developer Edition API reference is at http://<development-machine-ip>:19669/api-docs, where <development-machine-ip> is the IP address of the machine running Docker.

Code explanation

Query and response fields

The application queries the movie name first, then its actors. Checking the movie first distinguishes a missing movie from an existing movie with no actors: the former returns 404; the latter returns an empty actors array.

The actor query is:

USE /default_schema/movie MATCH (a@Actor)-[:Act]->(m@Movie)
WHERE m.id = 3
RETURN a.id AS actor_id, a.name AS actor ORDER BY actor_id

(a@Actor)-[:Act]->(m@Movie) matches an actor's relationship to a movie they act in. The arrow points from actor to movie. WHERE filters by movie ID, and ORDER BY sorts by actor ID. The application replaces 3 with the validated integer movie_id. See Example dataset: movie for the model.

Query result Application response field
m.name AS movie in the movie query movie
a.id AS actor_id in the actor query actor_id in each item of actors
a.name AS actor in the actor query actor in each item of actors

Authenticate and send queries

For each request, the application:

  1. Sends NebulaGraph Database Developer Edition credentials to POST /auth and reads the JWT from data.token.
  2. Sets Authorization: Bearer <token> and sends {"gql": "query statement"} to POST /gql.
  3. Checks the HTTP status and response body code. The success code is the string "00000"; an HTTP 200 response can still contain a query error. On success, result rows are read from data.tables.
  4. Calls POST /signout after the queries finish or fail to release the authenticated session, then closes the HTTP client.

post() and query_rows() are helpers defined in the complete code to check responses and read result rows:

auth = post(client, "/auth", {"username": username, "password": password})
client.headers["Authorization"] = f"Bearer {auth['token']}"
movies = query_rows(client, movie_query)
actors = query_rows(client, actors_query) if movies else []

data.headers lists the result column names; data.tables is an array of row objects. The query aliases fix the field names, so the application can read movie, actor_id, and actor directly.

Each query specifies the graph with USE /default_schema/movie, so no session working graph is required. Put multiline strings directly in the JSON gql field, without the ''' input delimiters used by ngql.

Response status and troubleshooting

HTTP status Meaning Action
200 The query succeeded Inspect movie and actors; the array is empty when there are no actors
404 No movie has this ID Use an ID present in the dataset, such as 3
422 The movie ID has an invalid format or range Use a positive integer no greater than 9223372036854775807
502 A NebulaGraph Database Developer Edition HTTP request, authentication, or query failed Check the NebulaGraph Database Developer Edition address and password, then use Connect and run your first query to confirm access to the data

To check input validation, run:

curl -i http://127.0.0.1:8100/movies/abc/actors

Expect HTTP 422. The request is rejected before any query is sent to NebulaGraph Database Developer Edition. The application accepts only a movie ID; the code fixes the query structure and graph path.

When the application is unreachable

If the browser reports ERR_CONNECTION_REFUSED or curl cannot connect, check:

  • The application terminal is still running.
  • The request targets the application computer. 127.0.0.1 always refers to the computer running the browser or command.
  • For SSH access, the local forwarding terminal is still running.

Run this command on the browser computer. Use the updated local port if you changed it:

curl -I http://127.0.0.1:8100/docs

Expect HTTP/1.1 200 OK, confirming that the application's API documentation is reachable. Then call the movie endpoint to check the NebulaGraph Database Developer Edition query results.

The application terminal logs the exception type for failed NebulaGraph Database Developer Edition requests. Responses sent to callers contain no NebulaGraph Database Developer Edition passwords or JWT tokens.

Exercise: query directors

Copy the get_actors() function and its @app.get decorator. Name the new function get_directors() and change its route to /movies/{movie_id}/directors. Keep the movie existence check and replace the actor query with:

USE /default_schema/movie MATCH (d@Director)-[:Direct]->(m@Movie)
WHERE m.id = 3
RETURN d.id AS director_id, d.name AS director ORDER BY director_id

Rename the actor query and result variables for directors. Return a directors array, with director_id and director in each item.

Restart the application and call the new endpoint to check the movie and its directors. An existing movie with no director relationships returns an empty directors array; a missing movie returns 404. Run the query in the NebulaGraph Studio console first to compare its result with the application response.

Continue development

Press Ctrl+C to stop the application. NebulaGraph Database Developer Edition continues running and retains its graph data. If you switch terminals before restarting, activate the virtual environment and set the environment variables again.

The example authenticates and signs out separately for each request. If your application reuses JWT tokens, handle expiration and renewal after authentication failures. See Configure user access for NebulaGraph Database Developer Edition privileges.