Skip to content
Browse files

Episode 26 - Creating a CLI for a REST API (#113)

* Creating a CLI for a REST API

* moving to ep 26

* re-add episode 20 Makefile

* spacing

* move to episode 26

* progress!

* mod tidy

* fixes

* more readme-ing

* Add reference to dcode

* better show notes

* adding ignore for ds binary

* Adding a web page for this episode

YouTube ID to come - still uploading

* YT video ID
  • Loading branch information...
arschles committed Jul 9, 2019
1 parent f3d9e07 commit c5287aa61c6db248b775300c579e4e88203cb638
@@ -23,6 +23,8 @@ glide


@@ -46,3 +48,5 @@ episode25/tmp


This file was deleted.

@@ -0,0 +1,4 @@
go test -test.v ./...
go build -o episode20
@@ -0,0 +1,41 @@
# Consuming a REST API in Go

Go in 5 Minutes, episode 26.

In this screencast, we're going to build a command line client to consume the awesome [Dark Sky API](

We'll be using [cobra]( to build our command line client, and I did a previous episode on that package. If you haven't seen [episode 18](, you might want to go review that before you look at this one.

Instead of using an already-built Dark Sky API client (there are a [few]( for Go), we're going to build our own client according to the API documentation to show some tips and tricks for building clients for any REST API.

In this screencast, we'll use the awesome [gorequest]( package to help us build a DarkSky client from scratch.

# Outline

1. Quick primer on Cobra
1. Quick primer on gorequest
1. Let's check out the code!

# How to Run This Code

You'll need Go version 1.11 or above to run this code. If you have an appropriate version, simply run `go build -o darksky .` to build.

Before you run the binary, you'll need an environment variable called `DARKSKY_API_KEY` set to your DarkSky API key (if you don't have one, get it from your [account](, or [create]( an account if you haven't already).

Then, call the binary like so, ensuring that `DARKSKY_API_KEY` is set in your environment:

$ ./darksky temp --lat 45.512230 --long -122.658722

The `lat` and `long` flags are set to the latitude and longitude (respectively) of the location for which to get the temperature.

>The latitude and longitude in the above example are set to Portland, OR, USA. If you'd like to try another location, you can use
# Show Notes

- [Dark Sky API](
- [gorequest](
- [Cobra CLI Package](
- [Cobra Code Generation CLI](
- A different way to decode JSON: [dcode](
@@ -0,0 +1,89 @@
package cmd

import (


homedir ""

var cfgFile string

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "episode26",
Short: "A brief description of your application",
Long: `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.`,
// Uncomment the following line if your bare application
// has an action associated with it:
// Run: func(cmd *cobra.Command, args []string) { },

// 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() {
if err := rootCmd.Execute(); err != nil {

func init() {
apiKey := os.Getenv("DARKSKY_API_KEY")
if apiKey == "" {
fmt.Println("No DARKSKY_API_KEY environment variable set")

// 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/.episode26.yaml)")

// Cobra also supports local flags, which will only run
// when this action is called directly.
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")

tempCmd, err := temp(apiKey)
if err != nil {

// initConfig reads in config file and ENV variables if set.
func initConfig() {
if cfgFile != "" {
// Use config file from the flag.
} else {
// Find home directory.
home, err := homedir.Dir()
if err != nil {

// Search config in home directory with name ".episode26" (without extension).

viper.AutomaticEnv() // read in environment variables that match

// If a config file is found, read it in.
if err := viper.ReadInConfig(); err == nil {
fmt.Println("Using config file:", viper.ConfigFileUsed())
@@ -0,0 +1,40 @@
package cmd

import (


func temp(apiKey string) (*cobra.Command, error) {
cl, err := dsclient.New(apiKey)
if err != nil {
return nil, err
cmd := &cobra.Command{
Use: "temp",
flags := cmd.PersistentFlags()
lat := flags.Float64("lat", 0, "The Latitude to fetch")
long := flags.Float64("long", 0, "The Longitude to fetch")
cmd.RunE = func(*cobra.Command, []string) error {
fmt.Printf("Getting temp for (%f, %f)\n", *lat, *long)
fcast, err := cl.Forecast(*lat, *long)
if err != nil {
return err
if len(fcast.Hourly.Data) < 1 {
return fmt.Errorf("No hourly data returned!")
hourly := fcast.Hourly.Data[0]
"Temperature for your location (%f, %f): %f\n",
hourly.ApparentTemp, // the API only allows apparent temp on hourly
return nil
return cmd, nil
@@ -0,0 +1,38 @@
package dsclient

import (

multierr ""

type Client struct {
apiKey string
cl *gorequest.SuperAgent

func New(apiKey string) (*Client, error) {
if apiKey == "" {
return nil, fmt.Errorf("No API key passed")
return &Client{apiKey: apiKey, cl: gorequest.New()}, nil

func (c *Client) Forecast(lat, long float64) (*Response, error) {
forecastURL := fmt.Sprintf(
resp := &Response{}
httpRes, _, errs :=
if len(errs) > 0 {
return nil, &multierr.Error{Errors: errs}
if httpRes.StatusCode != 200 {
return nil, fmt.Errorf("HTTP Status Code %d returned!", httpRes.StatusCode)
return resp, nil
@@ -0,0 +1,9 @@
package dsclient

type DataBlock struct {
Data []DataPoint `json:"data"`
Summary string `json:"summary"`
Icon string `json:"icon"`
// There are way more fields in a data block.
// see for all of them
@@ -0,0 +1,7 @@
package dsclient

type DataPoint struct {
ApparentTemp float64 `json:"apparentTemperature"`
// There are way more fields in a data point.
// see for all of them
@@ -0,0 +1,11 @@
package dsclient

type Response struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Timezone string `json:"timezone"`
Currently DataPoint `json:"currently"`
Minutely DataBlock `json:"minutely"`
Hourly DataBlock `json:"hourly"`
Daily DataBlock `json:"daily"`
@@ -0,0 +1,24 @@

go 1.12

require ( v0.0.0-20190703090003-6125c262ffb0 // indirect v0.0.0-20190703090003-6125c262ffb0 // indirect v1.0.0 v1.8.1 // indirect v1.1.0 v1.0.0 // indirect v0.2.15 v1.4.0 // indirect v0.8.1 // indirect v0.0.0-20190330032615-68dc04aab96a // indirect v1.2.2 // indirect v0.0.5 v1.1.0 // indirect v1.4.0 v1.3.0 // indirect v0.0.0-20190628185345-da137c7871d7 // indirect v0.0.0-20190626221950-04f50cda93cb // indirect v0.3.2 // indirect

0 comments on commit c5287aa

Please sign in to comment.
You can’t perform that action at this time.