Interacting with the Dev.to Article API

shindakun

Steve Layton

Posted on March 9, 2019

Interacting with the Dev.to Article API

ATLG Sidebar

Uhh OK

Hay all! If this is your first time, welcome! If not welcome back! This week I'm starting a new "series" here on Dev.to. This week so I decided to try to flex my muscle memory and write a small utility that would interact with an API. I'm using the dev.to/api/articles endpoint in this case. This dovetails into another side project I want to start poking around with! I need a chunk of data on hand for that (we'll come back to that on a different sidebar post if it ever comes together).


Code Walkthrough

package main

import (
  "encoding/json"
  "fmt"
  "io/ioutil"
  "net/http"
  "sync"
  "time"
)
Enter fullscreen mode Exit fullscreen mode

If you write Go, JSON-to-Go is going to be your friend. If we need to unmarshal JSON we can simply paste in a sample of the JSON and it will give you a struct. Nice. We don't actually need the entire struct we could have left only the ID int32 'json:"id"' as its the only one we use. I'm included the entire thing now, we may use it in the future.

// Articles array JSON struct
type Articles []struct {
  TypeOf                 string    `json:"type_of"`
  ID                     int32     `json:"id"`
  Title                  string    `json:"title"`
  Description            string    `json:"description"`
  CoverImage             string    `json:"cover_image"`
  PublishedAt            time.Time `json:"published_at"`
  TagList                []string  `json:"tag_list"`
  Slug                   string    `json:"slug"`
  Path                   string    `json:"path"`
  URL                    string    `json:"url"`
  CanonicalURL           string    `json:"canonical_url"`
  CommentsCount          int       `json:"comments_count"`
  PositiveReactionsCount int       `json:"positive_reactions_count"`
  User                   struct {
    Name            string      `json:"name"`
    Username        string      `json:"username"`
    TwitterUsername string      `json:"twitter_username"`
    GithubUsername  interface{} `json:"github_username"`
    WebsiteURL      string      `json:"website_url"`
    ProfileImage    string      `json:"profile_image"`
    ProfileImage90  string      `json:"profile_image_90"`
  } `json:"user"`
}
Enter fullscreen mode Exit fullscreen mode

I started to lay this out as if we were going to use it as an importable package. It could have been done mostly inline in main() but maybe we'll flesh this out into a full DEV API client. I'll have to take a look at the v0 API a bit closer and see what is actually supported.

// DevtoClient struct
type DevtoClient struct {
  DevtoAPIURL string
  Client      *http.Client
}

// New returns our DevtoClient
func New(apiurl string, client *http.Client) *DevtoClient {
  if client == nil {
    client = http.DefaultClient
  }
  return &DevtoClient{
    apiurl,
    client,
  }
}
Enter fullscreen mode Exit fullscreen mode

Formatting our requests this way might be overkill but, for a start, it puts us in a good spot. The requests can take a few different parameters but we aren't concerned with any of those. At least not this time around, we only want the articles themselves.

// FormatPagedRequest returns *http.Request ready to do() to get one page
func (dtc DevtoClient) FormatPagedRequest(param, paramValue string) (r *http.Request, err error) {
  URL := dtc.DevtoAPIURL + "articles/?" + param + "=" + paramValue
  fmt.Printf("%v\n", URL)
  r, err = http.NewRequest(http.MethodGet, URL, nil)
  if err != nil {
    return nil, err
  }
  return r, nil
}

// FormatArticleRequest returns http.Request ready to do() and get an article
func (dtc DevtoClient) FormatArticleRequest(i int32) (r *http.Request, err error) {
  URL := fmt.Sprintf(dtc.DevtoAPIURL+"articles/%d", i)
  r, err = http.NewRequest(http.MethodGet, URL, nil)
  if err != nil {
    return nil, err
  }
  return r, nil
}
Enter fullscreen mode Exit fullscreen mode

This time around I am experimenting with sync.waitGroup. WaitGroups allow us to kick off a series of Goroutines and wait for them to finish before moving on with the code. We'll see further on in the code when getArticle() executes as a Goroutine. It is what actually gets the article from the API and writes it to disk. This way we're grabbing one set of 30 article ids. As we parse those we begin getting the articles. Once we've received them all only then do we move on to the next set.

func getArticle(dtc *DevtoClient, i int32, wg *sync.WaitGroup) {
  defer wg.Done()
  r, err := dtc.FormatArticleRequest(i)
  if err != nil {
    panic(err)
  }

  resp, err := dtc.Client.Do(r)
  if err != nil {
    panic(err)
  }
  defer resp.Body.Close()

  body, err := ioutil.ReadAll(resp.Body)
  if err != nil {
    panic(err)
  }
  fileName := fmt.Sprintf("%d.json", i)
  ioutil.WriteFile("./out/"+fileName, body, 0666)
}
Enter fullscreen mode Exit fullscreen mode

main() is straightforward enough. We create our client, using http.DefaultClient. We've provided the ability to use an alternate configuration if we need it in the future. doit and c will be controlling our for loop and the main body of the program.

func main() {
  dtc := New("https://dev.to/api/", nil)
  doit := true
  c := 1
Enter fullscreen mode Exit fullscreen mode

In each run, through the loop, we get a single page of articles. We then set up our WaitGroup and our articles variable. Once we have unmarshalled the articles JSON we get the length of that array. That length tells WaitGroup how many "times" to wait. Note that we are calling defer wg.Done() as the first line in the getArticle(). This subtracts one from the WaitGroup total allowing us to move on when finished. The current Dev.to article API returns an empty array, [] when there is no data for a page. We check to see if we have that as a response and stop if so.

  for doit {
    req, err := dtc.FormatPagedRequest("page", fmt.Sprintf("%d", c))
    if err != nil {
      panic(err)
    }
    resp, err := dtc.Client.Do(req)
    if err != nil {
      panic(err)
    }
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
      panic(err)
    }

    var wg sync.WaitGroup
    var articles Articles

    json.Unmarshal(body, &articles)
    wg.Add(len(articles))

    for i := range articles {
      go getArticle(dtc, articles[i].ID, &wg)
    }
    wg.Wait()

    if string(body) != "[]" {
      c++
      continue
    }
    doit = false
  }
}
Enter fullscreen mode Exit fullscreen mode

Wrapping Up

There we go, the first "sidebar"! I'll probably be adding the code for this into the main "Attempting To Learn Go" repo over on GitHub. Now that I think about it I still need to post last weeks over there!

Have you done any API work in Go? Anything you would do a bit differently? Let me know in the comments below. Consructive comments are always welcome!


You can find the code for this and most of the other Attempting to Learn Go posts in the repo on GitHub.




💖 💪 🙅 🚩
shindakun
Steve Layton

Posted on March 9, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related