🎬 Build Your Own Movie CLI with Go — Fetch Live Data from TMDB!

After making my first Task Tracker CLI, I wanted something fun — so I decided to build a Movie CLI that fetches live data from TMDB (The Movie Database).
This project taught me about working with Cobra CLI, HTTP APIs, and JSON Marshal & Unmarshal in Go.
And yes, it’s as cool as it sounds — you can literally list trending or upcoming movies right from your terminal!
https://roadmap.sh/projects/tmdb-cli
Project URL : https://github.com/pranav767/GO_PROJECTS/tree/main/tmdb
Requirements
The application should run from the command line, and be able to pull and show the popular, top-rated, upcoming and now playing movies from the TMDB API. The user should be able to specify the type of movies they want to see by passing a command line argument to the CLI tool.
Here’s how it should look like
tmdb-app --type "playing"
tmdb-app --type "popular"
tmdb-app --type "top"
tmdb-app --type "upcoming"
TMDB api: https://developer.themoviedb.org/docs/getting-started
~/golang/tmbd
16:27:40 ❯ go mod init tmbd
go: creating new go.mod: module tmbd
~/golang/tmbd via 🐹 v1.19.6
Step 1: Initialize the Project & add cobra-cli
First, let’s install the Cobra library and the cobra-cli tool that will help us generate our application structure:
go get -u github.com/spf13/cobra@latest
go install github.com/spf13/cobra-cli@latest
The first command adds Cobra as a dependency, while the second installs the cobra-cli generator tool that makes creating new applications and commands much easier.
jinx@MSI:~/go/GO_PROJECTS/tmdb$ go get -u github.com/spf13/cobra@latest
go: downloading github.com/spf13/pflag v1.0.7
go: added github.com/inconshreveable/mousetrap v1.1.0
go: added github.com/spf13/cobra v1.9.1
go: added github.com/spf13/pflag v1.0.7
jinx@MSI:~/go/GO_PROJECTS/tmdb$ go install github.com/spf13/cobra-cli@latest
go: downloading github.com/spf13/cobra-cli v1.3.0
go: downloading github.com/spf13/cobra v1.3.0
go: downloading github.com/spf13/viper v1.10.1
go: downloading github.com/subosito/gotenv v1.2.0
go: downloading github.com/spf13/afero v1.6.0
go: downloading github.com/spf13/jwalterweatherman v1.1.0
go: downloading github.com/spf13/cast v1.4.1
go: downloading github.com/spf13/pflag v1.0.5
go: downloading github.com/mitchellh/mapstructure v1.4.3
go: downloading github.com/fsnotify/fsnotify v1.5.1
go: downloading gopkg.in/ini.v1 v1.66.2
go: downloading github.com/magiconair/properties v1.8.5
go: downloading github.com/pelletier/go-toml v1.9.4
go: downloading gopkg.in/yaml.v2 v2.4.0
go: downloading github.com/hashicorp/hcl v1.0.0
go: downloading golang.org/x/sys v0.0.0-20211210111614-af8b64212486
go: downloading golang.org/x/text v0.3.7
jinx@MSI:~/go/GO_PROJECTS/tmdb$ cobra-cli init tmdb
Your Cobra application is ready at
/home/jinx/go/GO_PROJECTS/tmdb/tmdb
jinx@MSI:~/go/GO_PROJECTS/tmdb$ ls -la
total 20
drwxr-xr-x 3 jinx jinx 4096 Aug 13 2025 .
drwxr-xr-x 10 jinx jinx 4096 Aug 13 2025 ..
-rw-r--r-- 1 jinx jinx 179 Aug 13 2025 go.mod
-rw-r--r-- 1 jinx jinx 981 Aug 13 2025 go.sum
drwxr-xr-- 3 jinx jinx 4096 Aug 13 2025 tmdb
Need to add
export PATH="$PATH:$(go env GOPATH)/bin"
add to ~/.bashrc and do a source
jinx@MSI:~/go/GO_PROJECTS/tmdb$ go mod init tmdb
go: /home/jinx/go/GO_PROJECTS/tmdb/go.mod already exists
jinx@MSI:~/go/GO_PROJECTS/tmdb$ cobra-cli init
Your Cobra application is ready at
/home/jinx/go/GO_PROJECTS/tmdb
jinx@MSI:~/go/GO_PROJECTS/tmdb$ go run main.go --help
A longer description that spans multiple lines and likely contains
examples and usage of using your application. For example:
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.
This will create following project structure

jinx@MSI:~/go/GO_PROJECTS/tmdb$ cobra-cli --help
Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.
Usage:
cobra-cli [command]
Available Commands:
add Add a command to a Cobra Application
completion Generate the autocompletion script for the specified shell
help Help about any command
init Initialize a Cobra Application
Flags:
-a, --author string author name for copyright attribution (default "YOUR NAME")
--config string config file (default is $HOME/.cobra.yaml)
-h, --help help for cobra-cli
-l, --license string name of license for the project
--viper use Viper for configuration
Use "cobra-cli [command] --help" for more information about a command.
Understanding root.go file in cobra-cli
For the goal of this project we do not need to have a additional command, we can just use root.go
Update the root.go file
/*
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
*/
package cmd
import (
"fmt"
"os"
"tmdb/tmdbapi"
"github.com/spf13/cobra"
)
var movieType string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "tmdb [--type <movie_type>]",
Short: "A CLI tool for The Movie Database (TMDB)",
Long: `A command line interface for fetching movie data from TMDB.
Supports different movie categories like playing, popular, top rated, and upcoming.
Example usage:
tmdb --type playing
tmdb --type popular
tmdb --type top_rated
tmdb --type upcoming`,
Run: func(cmd *cobra.Command, args []string) {
switch movieType {
case "playing":
tmdbapi.Playing()
case "popular":
// Fetch and display popular movies
tmdbapi.Popular()
case "top_rated":
// Fetch and display top-rated movies
tmdbapi.TopRated()
case "upcoming":
// Fetch and display upcoming movies
tmdbapi.Upcoming()
default:
fmt.Println("Unknown movie type. Please use one of: playing, popular, top_rated, upcoming.")
os.Exit(1)
}
},
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
// Here you will define your flags and configuration settings.
// Cobra supports persistent flags, which, if defined here,
// will be global for your application.
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.tmdb.yaml)")
// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().StringVarP(&movieType, "type", "t", "", "Type of movies to fetch (playing, popular, top_rated, upcoming)")
rootCmd.MarkFlagRequired("type")
//rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
So, cobra-cli almost sets you up for the required cli, we want following cli results
tmdb-app --type "playing"
tmdb-app --type "popular"
tmdb-app --type "top"
tmdb-app --type "upcoming"
—type here is a flag and not a argument/parameter, so we don’t need to use command cobra-cli add
To have a —type flag, we need to use
rootCmd.Flags().StringVarP(&movieType, "type", "t", "", "Type of movies to fetch (playing, popular, top_rated, upcoming)")
This stores the flag’s value in variable movieType, flag name would be type and could be used short as t, default value is stored as empty string ““, then there is a short description of the flag, after this we’ll be using
rootCmd.MarkFlagRequired("type")
This marks the flag as a required bit, the cli will fail with error if —type flag is not used.
Inside rootCmd we’ll be setting up how the cli is used, small description(Short:), usage(Use:) and the main logic (Run:)
Inside the main logic we’re setting up a switch case for the type flag, that is for the values of flag, incase the flag is popular, top_rated, playing, upcoming .
Furthermore we want to keep the logic/working of each calls in different file.
Project Structure

Writing API calls for all movie filters
tmdbapi/api.go to add making api requests logic, Also note this that to access functions written in api.go , we need to add the following in root.go
import (
"tmdb/tmdbapi"
)
Now our api.go will look like:
package tmdbapi
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
type Movie struct {
Title string `json:"title"`
}
type ApiResponse struct {
Results []Movie `json:"results"`
}
func fetchAndPrintMovies(url string, label string) {
apiKey := os.Getenv("TMDB_API_KEY")
if apiKey == "" {
fmt.Println("Please set the TMDB_API_KEY environment variable.")
os.Exit(1)
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
// Make the request
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error making request:", err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error while reading response body:", err)
return
}
// Parse the JSON response
var apiResp ApiResponse
if err := json.Unmarshal(body, &apiResp); err != nil {
fmt.Println("Error parsing JSON:", err)
return
}
if len(apiResp.Results) == 0 {
fmt.Printf("No %s movies found.\n", label)
return
}
fmt.Printf("%s Movies:\n", label)
for _, movie := range apiResp.Results {
fmt.Println("- " + movie.Title)
}
}
func Playing() {
// Implement the logic to fetch currently playing movies
fmt.Println("Fetching currently playing movies...")
url := "https://api.themoviedb.org/3/movie/now_playing?language=en-US&page=1"
fetchAndPrintMovies(url, "Currently Playing")
}
func Popular() {
fmt.Println("Fetching popular movies...")
url := "https://api.themoviedb.org/3/movie/popular?language=en-US&page=1"
fetchAndPrintMovies(url, "Popular")
}
func TopRated() {
fmt.Println("Fetching top-rated movies...")
url := "https://api.themoviedb.org/3/movie/top_rated?language=en-US&page=1"
fetchAndPrintMovies(url, "Top Rated")
}
func Upcoming() {
fmt.Println("Fetching upcoming movies...")
url := "https://api.themoviedb.org/3/movie/upcoming?language=en-US&page=1"
fetchAndPrintMovies(url, "Upcoming")
}
Let us go line by line about what this file does,
we set up by giving package a name tmdbapi, write all required imports
Import Packages
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
)
“encoding/json” would be required to parse and see required outputs from the api request made, “fmt” is just printing statements, “io” to read request body , “net/http” used to make http requests, “os” used to kill the programs in case of failures.
Golang Structures
We only want titles of those movies so creating a struct,
type Movie struct {
Title string `json:"title"`
}
type ApiResponse struct {
Results []Movie `json:"results"`
}
ApiResponse is a slice of struct Movie(which has titles as string)
Fetch & Print Movies
Now,writing the main logic to make the api calls for popular,top_rated,now playing, upcoming movies, so the logic would be same for all the calls, it will be just a difference in the api url ,other stuff like using a API_KEY, making a http request, reading the response body, parsing the response and printing list of movies would be same so we’ll create a common helper/fetchAndPrintMovies function which takes in the url as a input/parameter and we can write the common logic here,
func fetchAndPrintMovies(url string, label string) {
apiKey := os.Getenv("TMDB_API_KEY")
if apiKey == "" {
fmt.Println("Please set the TMDB_API_KEY environment variable.")
os.Exit(1)
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
// Make the request
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error making request:", err)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error while reading response body:", err)
return
}
// Parse the JSON response
var apiResp ApiResponse
if err := json.Unmarshal(body, &apiResp); err != nil {
fmt.Println("Error parsing JSON:", err)
return
}
if len(apiResp.Results) == 0 {
fmt.Printf("No %s movies found.\n", label)
return
}
fmt.Printf("%s Movies:\n", label)
for _, movie := range apiResp.Results {
fmt.Println("- " + movie.Title)
}
}
This function first checks if API key is set in the current terminal, you can set your api key from https://www.themoviedb.org/settings/api
Use API Read Access Token for getting the API key, set API key as
export TMDB_API_KEY="blah"
Using http package in golang
Just create a new http GET request , and set all the required headers, in this case it would be auth token and content-type
req, err := http.NewRequest("GET", url, nil)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
Make the request and ensure that the response body is closed when the function returns using defer.
// Make the request
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error making request:", err)
return
}
defer resp.Body.Close()
Using io package to read
Read the response body and parse the response in the json struct that we created as ApiResponse
body, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error while reading response body:", err)
return
}
// Parse the JSON response
var apiResp ApiResponse
if err := json.Unmarshal(body, &apiResp); err != nil {
fmt.Println("Error parsing JSON:", err)
return
}
Print the list of all movies using range
if len(apiResp.Results) == 0 {
fmt.Printf("No %s movies found.\n", label)
return
}
fmt.Printf("%s Movies:\n", label)
for _, movie := range apiResp.Results {
fmt.Println("- " + movie.Title)
}
Function for movie filters
Now we just need to create functions for all the values of the type flag, having the url & label for that http request call
func Playing() {
// Implement the logic to fetch currently playing movies
fmt.Println("Fetching currently playing movies...")
url := "https://api.themoviedb.org/3/movie/now_playing?language=en-US&page=1"
fetchAndPrintMovies(url, "Currently Playing")
}
Inside root.go we just need to call the above created functions in api.go, remember we used import ‘tmdb/tmdbapi” in root.go, this will help us access functions created in api.go
Using switch case in golang
Run: func(cmd *cobra.Command, args []string) {
switch movieType {
case "playing":
tmdbapi.Playing() // Call the Playing function from tmdbapi package
// Fetch and display currently playing movies
fmt.Println("Fetching currently playing movies...")
case "popular":
// Fetch and display popular movies
tmdbapi.Popular() // Assuming you have a Popular function in tmdbapi
case "top_rated":
// Fetch and display top-rated movies
tmdbapi.TopRated() // Assuming you have a TopRated function in tmdbapi
case "upcoming":
// Fetch and display upcoming movies
tmdbapi.Upcoming() // Assuming you have an Upcoming function in tmdbapi
default:
// Handle unknown movie types
fmt.Println("Unknown movie type. Please use one of: playing, popular, top_rated, upcoming.")
os.Exit(1)
}
},
Now we are ready to use our cli:
jinx@MSI:~/go/GO_PROJECTS/tmdb$ go run main.go --type playing
Fetching currently playing movies...
Currently Playing Movies:
- War of the Worlds
- Jurassic World Rebirth
- William Tell
- The Pickup
- Legends of the Condor Heroes: The Gallants
- Night Carnage
- Demon Slayer: Kimetsu no Yaiba Infinity Castle
- Weapons
- M3GAN 2.0
- My Oxford Year
- Karate Kid: Legends
- Screamboat
- Superman
- The Fantastic 4: First Steps
- Ballerina
- Guns Up
- Happy Gilmore 2
- Abraham's Boys: A Dracula Story
- Hostile Takeover
- Bride Hard
Fetching currently playing movies...
jinx@MSI:~/go/GO_PROJECTS/tmdb$ go run main.go --type top_rated
Fetching top-rated movies...
Top Rated Movies:
- The Shawshank Redemption
- The Godfather
- The Godfather Part II
- Schindler's List
- 12 Angry Men
- Spirited Away
- The Dark Knight
- Dilwale Dulhania Le Jayenge
- The Green Mile
- Parasite
- The Lord of the Rings: The Return of the King
- Pulp Fiction
- Your Name.
- Forrest Gump
- The Good, the Bad and the Ugly
- Interstellar
- Seven Samurai
- GoodFellas
- Grave of the Fireflies
- Life Is Beautiful
In case there is some network error, our cli ends with displaying error
jinx@MSI:~/go/GO_PROJECTS/tmdb$ go run main.go --type upcoming
Fetching upcoming movies...
Error making request: Get "https://api.themoviedb.org/3/movie/upcoming?language=en-US&page=1": dial tcp 49.44.79.236:443: i/o timeout
jinx@MSI:~/go/GO_PROJECTS/tmdb$ go run main.go --type upcoming
Fetching upcoming movies...
Upcoming Movies:
- How to Train Your Dragon
- Demon Slayer: Kimetsu no Yaiba Infinity Castle
- Weapons
- Ne Zha 2
- Karate Kid: Legends
- Ballerina
- KPop Demon Hunters
- Bride Hard
- Final Destination Bloodlines
- Freakier Friday
- The Bad Guys 2
- Bring Her Back
- Together
- Materialists
- The Ritual
- The Naked Gun
- Dangerous Animals
- Sorry, Baby
- The Occupant
- Clown in a Cornfield
That is it for this CLI tutorial. 🙌
Ciao 👋



