Building a Movie Recommendation App with Modus, Dgraph & Next.js

Discover how easy it is to build a serverless, data-rich, AI-enhanced application

In this guide, we’ll walk through how to build a movie recommendation app using Modus and Dgraph with Go.

This template allows users to search for movies, view details, and get AI-driven recommendations based on a large language model (LLM). By leveraging Dgraph's read-only movie dataset (available here), you get started quickly with minimal setup required for Dgraph on your end.

Using Modus, we seamlessly integrate Dgraph's graph database with an LLM to combine structured internal data with intelligent AI responses. This integration makes it easy to build serverless, data-rich, and AI-enhanced applications. Throughout this guide, we’ll walk you through writing the backend using Go and linking it to a front-end application using Next.js, resulting in a complete, functional movie recommendation app. You can find the complete codebase for both the backend and front-end here on GitHub.

Prerequisites

Before diving in, ensure you have the following:

Resources

Here are some resources to get you started and expand your knowledge:

Building the Backend

Modus

Modus is an open source, serverless framework that simplifies integrating AI models and APIs into applications. It allows developers to focus on building intelligent features without the overhead of complex infrastructure. Here’s why Modus stands out:

  • Seamless AI Integration: Modus makes it simple to integrate AI models like LLMs, treating them as first-class components to reduce complexity in development.

  • Database Integration: Offers robust support for querying and mutating data from databases like Dgraph, enabling the combination of structured data with AI capabilities.

  • Advanced Search: Provides semantic and similarity-based search capabilities, leveraging vector embeddings to enhance natural language search and recommendations.

  • Optimized Performance: Designed for real-time responses, making it ideal for applications that need speed and reliability.

Dgraph

Dgraph is a high-performance, distributed graph database built to manage complex, interconnected data. It excels in running queries involving relationships between entities, making it ideal for highly connected datasets like movies. For example, a single movie might be connected to multiple genres, several actors, and a director, while those actors might be connected to other movies, forming intricate relationships.

In this project, we use a read-only Dgraph dataset of movies, which is already set up and ready to use. This means no additional configuration is required on your end. You can explore the dataset directly via the Ratel interface or use queries to examine its structure.

Dgraph simplifies querying through its graph structure, enabling seamless integration with Modus for intelligent AI interactions. Its ability to handle complex relationships — such as linking movies, genres, actors, and directors — makes it an excellent choice for movie datasets.

You can view the schema of the dataset by running the following query in the Ratel Interface:

schema {}

Here are some queries you can run in the Ratel Interface to test it out.

Fetch the first 10 movies:

{
  movies(func: has(name@en), first: 10) {
    uid
    name@en
    genre {
      name@en
    }
  }
}

Search for a movie by name:

{
  movies(func: alloftext(name@en, "Inception")) {
    name@en
    genre {
      name@en
    }
  }
}

Setting Up Your Project

  1. Install the Modus CLI

npm install -g @hypermode/modus-cli

💡 Make sure you have Node.js installed with v22 or higher

  1. Initialize your Modus app

modus new

This command prompts you to choose between Go and AssemblyScript as the language for your app. In this example, we use Go. Modus then creates a new directory with the necessary files and folders for your app. You will also be asked if you would like to initialize a Git repository.

  1. Build and run your app

modus dev

This command builds and runs your app locally in development mode and provides you with a URL to access your app’s generated API.

  1. Access your local endpoint

    Once your app is running, you can access the graphical interface for your API at the URL located in your terminal.

View endpoint: http://localhost:8686/explorer

This interface allows you to interact with your app’s API and test your functions.

Configuring Your Project for the Model & Dgraph

To integrate an LLM and connect your project to Dgraph, you'll need to configure the modus.json file. This file acts as the manifest for your Modus project, defining the available models, endpoints, and data connections.

Add the following to your modus.json file to define the LLM model and the Dgraph connection:

{
  "$schema": "https://schema.hypermode.com/modus.json",
  "endpoints": {
    "default": {
      "type": "graphql",
      "path": "/graphql",
      "auth": "bearer-token"
    }
  },
  "models": {
    "text-generator": {
      "sourceModel": "meta-llama/Meta-Llama-3.1-8B-Instruct",
      "provider": "hugging-face",
      "connection": "hypermode"
    }
  },
  "connections": {
    "play": {
      "type": "http",
      "baseUrl": "https://play.dgraph.io/"
    }
  }
}
  • Endpoints: Defines the GraphQL endpoint for your Modus project. This is where your backend functions will be exposed.

  • Models: Configures the LLM you will use for generating recommendations. In this case:

    • The sourceModel is the Llama model provided by Hugging Face.

    • The connection is set to hypermode, which ensures that Modus handles communication with the provider.

  • Connections: Sets up the Dgraph connection using the play configuration. The baseUrl points to the read-only dataset hosted on Dgraph's public instance. We are using the HTTP endpoint here because the gRPC endpoint is not exposed for this public example. For production use, the recommended approach is to use the built-in Dgraph client for better performance and feature support.

Writing Your Functions

In Modus, your functions are written in the main.go file. To export a function so it can be accessed externally, simply capitalize its name. This project includes two exported functions: FetchMoviesWithPaginationAndSearch and FetchMovieDetailsAndRecommendations. These functions demonstrate how to query a Dgraph database and integrate its results with an AI model using Modus.

Let’s walk through how each of these functions is written, along with their supporting helper functions.

Imports

import (
	"encoding/json"
	"fmt"
	"strings"

	"github.com/hypermodeinc/modus/sdk/go/pkg/http"
	"github.com/hypermodeinc/modus/sdk/go/pkg/models"
	"github.com/hypermodeinc/modus/sdk/go/pkg/models/openai"

  • Standard Libraries:

    • encoding/json: Used for parsing and encoding JSON data.

    • fmt: Provides formatted I/O functions, such as Printf and Sprintf.

    • strings: Contains utility functions for string manipulation, like TrimSpace.

  • Modus-Specific Packages:

    • http: Provides tools for making HTTP requests, used here for querying Dgraph.

    • models: Enables integration with AI models defined in the modus.json file.

    • openai: Provides specialized tools for working with OpenAI models through Modus.

The FetchMoviesWithPaginationAndSearch Function

The FetchMoviesWithPaginationAndSearch function is a great example of how Dgraph simplifies working with complex, interconnected datasets.

The function retrieves movies along with related data like genres, actors, and directors, all in a single query. This eliminates the need for complex joins and multiple calls typical in relational databases.

Code breakdown:

func FetchMoviesWithPaginationAndSearch(page int, search string) (string, error) {
	offset := (page - 1) * 10 // Calculate offset for pagination (10 movies per page)

	// Build the query dynamically with pagination, sorting, and optional search filtering
	query := fmt.Sprintf(`{
		movies(func: has(initial_release_date), first: 10, offset: %d, orderdesc: initial_release_date) %s {
			uid
			name@en
			initial_release_date
			genre {
				name@en
			}
			starring {
				performance.actor {
					name@en
				}
			}
			directed_by: director.film {
				name@en
			}
		}
	}`, offset, buildSearchFilter(search))

	return executeDgraphQuery(query

  • Pagination: The offset variable calculates the starting point for the query based on the current page, fetching 10 movies per page.

  • Sorting: The query uses orderdesc to sort movies by release date, ensuring the most recent movies appear first.

  • Dynamic Filtering: If a search term is provided, the buildSearchFilter helper generates a Dgraph @filter clause to search across fields like titles, genres, actors, and directors.

Helper functions:

  • buildSearchFilter: This helper function dynamically builds a filter clause for the query:

func buildSearchFilter(search string) string {
	if search == "" {
		return ""
	}

	// Generate a Dgraph filter that matches the search term against relevant fields
	return fmt.Sprintf(`@filter(
		anyoftext(name@en, "%[1]s") OR
		anyoftext(genre.name@en, "%[1]s") OR
		anyoftext(starring.performance.actor.name@en, "%[1]s") OR
		anyoftext(directed_by.name@en, "%[1]s")
	)`, search

The filter matches the search term against multiple fields, showcasing Dgraph’s flexibility in handling queries for interconnected data. Its anyoftext function enables lightweight, full-text search across multiple attributes without additional indexing.

  • anyoftext: performs a full-text search on the specified field to find matches containing the search term. It is particularly useful for attributes like names or descriptions. The use of multiple anyoftext operations connected with OR enables searching across several attributes without requiring separate queries for each field.

💡 Note: Ensure that the Dgraph schema supports the fields referenced (e.g., genre.name@en, starring.performance.actor.name@en, directed_by.name@en). Misalignment between the schema and the query will result in no data or errors.

  • executeDgraphQuery: This function executes the query against the Dgraph database using Modus's HTTP client:

func executeDgraphQuery(query string) (string, error) {
	queryPayload := map[string]string{"query": query}

	options := &http.RequestOptions{
		Method: "POST",
		Body:   queryPayload,
		Headers: map[string]string{
			"Content-Type": "application/json",
		},
	}

	request := http.NewRequest("https://play.dgraph.io/query?respFormat=json", options)
	response, err := http.Fetch(request)
	if err != nil {
		fmt.Println("Error fetching data from Dgraph:", err)
		return "", err
	}

	return string(response.Body), nil

The Modus HTTP client abstracts the complexities of making network requests, ensuring seamless communication with the Dgraph database. In this example, we are using the HTTP endpoint because the gRPC endpoint is not exposed for the public Dgraph dataset. For production applications, the recommended approach is to use the built-in Modus Dgraph client, which supports gRPC and provides enhanced performance and features.

The FetchMovieDetailsAndRecommendations Function

The FetchMovieDetailsAndRecommendations function combines the querying power of Dgraph with the AI capabilities of Modus to retrieve movie details and generate AI-driven recommendations. This function exemplifies how internal data and AI models can work together seamlessly.

Code breakdown:

func FetchMovieDetailsAndRecommendations(uid string, searchQuery string) (string, error) {
	movieDetailsJSON, err := fetchMovieDetails(uid)
	if err != nil {
		return "", err
	}

	movieName, err := parseMovieName(movieDetailsJSON)
	if err != nil {
		return "", err
	}

	prompt := generatePrompt(movieName, searchQuery)

	recommendations, err := generateRecommendations(prompt)
	if err != nil {
		return "", fmt.Errorf("error generating recommendations: %w", err)
	}

	combinedResponse := fmt.Sprintf(`{
		"movieDetails": %q,
		"recommendations": %q
	}`, movieDetailsJSON, *recommendations)

	return combinedResponse, nil

  • Fetch Movie Details: The movie details are retrieved using fetchMovieDetails, which queries the Dgraph database for information like the movie's title, genres, actors, and director.

  • Parse Movie Name: The movie name is extracted from the JSON response with parseMovieName, ensuring that the details are correctly formatted for generating a prompt.

  • Generate Prompt: A natural language prompt is dynamically created using generatePrompt, combining the movie name and the user's search query to guide the LLM.

  • Generate Recommendations: The generateRecommendations function invokes the LLM configured in Modus to produce tailored movie recommendations.

  • Combine Results: The movie details and AI-generated recommendations are merged into a single JSON response for easy use on the frontend.

Helper Functions:

  • fetchMovieDetails: This helper queries the Dgraph graph database for detailed information about a specific movie using its uid:

func fetchMovieDetails(uid string) (string, error) {
	query := fmt.Sprintf(`{
		movie(func: uid(%s)) {
			uid
			name@en
			initial_release_date
			genre {
				name@en
			}
			starring {
				performance.actor {
					name@en
				}
			}
			directed_by: director.film {
				name@en
			}
		}
	}`, uid)

	return executeDgraphQuery(query

This function showcases Dgraph’s ability to fetch deeply interconnected data in a single query, reducing complexity and ensuring efficient data retrieval.

  • parseMovieName: After fetching the movie details, this function extracts the movie’s name from the JSON response:

func parseMovieName(movieDetailsJSON string) (string, error) {
	var parsedDetails struct {
		Data struct {
			Movie []MovieDetails `json:"movie"`
		} `json:"data"`
	}

	if err := json.Unmarshal([]byte(movieDetailsJSON), &parsedDetails); err != nil {
		return "", fmt.Errorf("error parsing movie details JSON: %w", err)
	}

	if len(parsedDetails.Data.Movie) == 0 || parsedDetails.Data.Movie[0].Name == "" {
		return "", fmt.Errorf("movie name not found in details")
	}

	return parsedDetails.Data.Movie[0].Name, nil

Parsing ensures that the data retrieved from Dgraph is structured and ready for use in generating the AI prompt.

  • generateRecommendations: this function leverages the LLM through Modus to generate AI-powered recommendations based on the movie name and user query:

func generateRecommendations(prompt string) (*string, error) {
	model, err := models.GetModel[openai.ChatModel]("text-generator")
	if err != nil {
		return nil, fmt.Errorf("error fetching model: %w", err)
	}

	input, err := model.CreateInput(
		openai.NewSystemMessage("You are a movie recommendation assistant. Provide concise suggestions."),
		openai.NewUserMessage(prompt),
	)
	if err != nil {
		return nil, fmt.Errorf("error creating model input: %w", err)
	}

	input.Temperature = 0.7

	output, err := model.Invoke(input)
	if err != nil {
		return nil, fmt.Errorf("error invoking model: %w", err)
	}

	outputStr := strings.TrimSpace(output.Choices[0].Message.Content)
	return &outputStr, nil

The integration of Modus makes it simple to feed internal data into the AI model, ensuring that the recommendations are contextual and relevant.

Testing Locally

To test your application locally, revisit the local API explorer at http://localhost:8686/explorer. This tool provides a convenient interface for interacting with the backend functions.

  1. Open the API Explorer in your browser.

  1. You will see the two exported functions, FetchMoviesWithPaginationAndSearch and FetchMovieDetailsAndRecommendations, listed as available operations.

  1. Select one of the functions to test. Fill in the required arguments:

    • For FetchMoviesWithPaginationAndSearch, provide a page number and an optional search query.

    • For FetchMovieDetailsAndRecommendations, enter a valid movie uid and an optional search query.

  2. Click Run to execute the function. The response will display below, showing the data retrieved from Dgraph or recommendations generated by the AI model.

This process ensures your backend is working as expected and allows you to explore its functionality in real time.

Integrating into a Front-end

The front-end for this application is built with Next.js and Tailwind CSS, but the integration process is not limited to these tools. The integration point lies in connecting the Modus backend via its GraphQL API, which can be adapted to any front-end framework.

Key Integration Points

The main integration between the frontend and backend happens in the actions.ts file, which serves as the utility layer for querying the Modus backend. This file uses fetch to communicate with the GraphQL API and handles request/response processing. The primary integration point is the fetchQuery function, which encapsulates the logic for sending GraphQL queries to the backend.

Here’s the fetchQuery function:

"use server";

type FetchQueryProps = {
  query: string;
  variables?: any;
};

const fetchQuery = async ({ query, variables }: FetchQueryProps) => {
  try {
    const res = await fetch(process.env.HYPERMODE_API_ENDPOINT as string, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ query, variables }),
      cache: "no-store",
    });

    if (!res.ok) throw new Error(res.statusText);

    const { data, errors } = await res.json();
    if (errors) throw new Error(JSON.stringify(errors));

    return { data };
  } catch (err) {
    console.error("Error in fetchQuery:", err);
    return { data: null, error: err };
  }
};

This function simplifies sending GraphQL queries by accepting the query and variables as parameters, making it reusable across the application. It also ensures consistent error handling and response parsing.

Example Action: Fetch Movies

Here’s an example action that uses fetchQuery to fetch a paginated list of movies:

export async function fetchMovies(page: number = 1, search: string = "") {
  const graphqlQuery = `
    query FetchMovies($page: Int!, $search: String!) {
      fetchMoviesWithPaginationAndSearch(page: $page, search: $search)
    }
  `;

  const { data, error } = await fetchQuery({
    query: graphqlQuery,
    variables: { page, search },
  });

  if (error) {
    console.error("Error fetching movies:", error);
    return { movies: [] };
  }

  try {
    const parsedData = JSON.parse(data.fetchMoviesWithPaginationAndSearch);
    return { movies: parsedData.data.movies || [] };
  } catch (err) {
    console.error("Error parsing response:", err);
    return { movies: [] };
  }
}

This action sends the FetchMovies query to the backend with pagination and search parameters, processes the response, and parses the data into a usable format. By using fetchQuery, the implementation remains clean and consistent, highlighting the seamless integration of the front-end with the Modus backend.

You can use similar actions to fetch other data, such as movie details and recommendations, leveraging the modularity provided by fetchQuery.

Using Actions in Front-end Pages

The actions.ts file simplifies backend interaction, allowing these functions to be used directly in the front-end. Here's an example of how these actions are used in a Next.js page:

import { fetchMovies } from "./actions";
import Link from "next/link";

export default async function Home({ searchParams }: { searchParams: any }) {
  const currentPage = Number(searchParams.page) || 1;
  const searchQuery = searchParams.search || "";

  const response = await fetchMovies(currentPage, searchQuery);

  return (
    <div className="p-6">
      <h1 className="text-2xl font-bold mb-4">Movies</h1>
      <SearchForm searchQuery={searchQuery} />
      <MoviesGrid movies={response.movies} searchQuery={searchQuery} />
    </div>
  );
}

This structure makes it easy to build dynamic pages for searching movies and viewing details with AI-generated recommendations.

Running Your App Locally

To test the app locally:

  1. Navigate to the frontend directory, install dependencies, and run the development server:

cd frontend
npm install
npm

  1. Ensure the backend is running locally to connect the front-end with the API.

  2. Open http://localhost:3000 in your browser. Now, you can:

    • Search for movies using the search bar.

    • Click on a movie to view details and AI-generated recommendations based on the selected movie and search query.

This setup demonstrates the seamless integration between the Modus backend and a modern front-end application.

Conclusion

This project demonstrates how Modus and Dgraph simplify building AI-driven applications by combining structured data with intelligent recommendations.

By leveraging Modus for seamless AI integration and Dgraph for querying interconnected datasets, you can create powerful, scalable solutions. Start exploring the codebase to adapt this template for your own projects!


Don't miss a single update: Star the Modus GitHub repo to stay in the loop with every new feature we ship.

Keep Me Updated

Stay updated

Hypermode Inc. © 2024

Stay updated

Hypermode Inc. © 2024

Stay updated

Hypermode Inc. © 2024