My Exciting And Harrowing Journey With The Google Civic Information API

And How It Empowered the Development of LegisLink

Well hello there :)

I hope you've had a fantastic start to your 2024, half-hearted new years resolutions and all! With the numerous holidays behind us, I'm officially starting on a weekly, Saturday schedule for this blog.

You may or may not have read my previous (and first!) blog post - where I discuss the conceptualization for my side project, an app that would better inform the American public on their elected officials: LegisLink. If you haven't read that introductory post, I'd highly recommend it! It's the important start to this journey, and I think it includes some valuable context to this post.

Regardless, I am excited to get into some technical depth with this post.

Why use the Google Civic Information API?

When planning the development of LegisLink, I wanted to create a MVP as fast as possible in regards to the core feature: grabbing information about members of Congress, based on the user's address. At the time, using the Google Civic Information API was the best way I knew. (Fun fact, a few years ago I had tried to use the API for a similar project in Python, but I couldn't figure out how to do API calls. I laugh about that now, and am proud of the progress I've made since then!).

Beyond the familiarity, the important part is that all it needs is an API key and a string representing a user's address. Thus making it the perfect candidate for my app.

What exactly is the Google Civic Information API?

A beautiful question! Of course, before I try and explain my understanding of it, here is a handy-dandy link to the documentation from Google:

https://developers.google.com/civic-information

The Google Civic Information API appears to be a collaboration project between Google and The Voting Information Project, an organization that "helps voters find information about their elections with collaborative, open-source tools." according to their website. It is through this partnership that The Voter Information project approves the election data that is eventually used by the Google Civic Information API, which is then consumed by well-to-do developers such as myself. Very cool.

This API doesn't just provide information regarding specific American officials, it also provides valuable data about elections. Per the documentation:
"During supported elections, you can also look up polling places, early vote location, candidate data, and other election official information." So, not only can you get information about an elected official by providing a postal address to the API, you can also get vital data regarding elections. All in all, this is a wonderful tool for delivering data to applications seeking to assist voter engagement.

In fact, I'd go so far as to say that this is one of the tools that actually falls under the "don't be evil" mantra that Google has touted in the past.

How does the Google Civic Information API work?

The long and short of it is that the Google Civic Information API provides information regarding elections and elected officials in a JSON format. And as mentioned earlier, the developer is empowered to do so by providing a postal address, and then getting the correct and up-to-date information about the elected officials in that area. Of course, the other benefit of this API is that up to 25,000 queries per day are free! For a work-in-progress app such as LegisLink, that is perfect. Google's documentation also provides more detail:

"Our data is based on the political geography of a citizen’s address. This address indicates where a citizen is eligible to vote and who represents them. There are many U.S. elections throughout the year, and both election information and political geography can shift with time. Google assigns every election available in the API an election ID, and the information associated with that ID is intended to be accurate for that election only."

Boom! Now for the cool part, how did I use it for LegisLink?

This API was honestly the starting point for the app from a conceptual and technical level. During my brief period of research, it was the only flexible and free API that could get me someone's congresspeople based on their address. So with that limitation it became an obvious choice. With that being said, I'll start by showing how I currently use the Google Civic Information API, henceforth referred to as the "GCI API".

From an architectural standpoint, I've generally followed MVVM principles and have also separated my API's into services. In LegisLink, that means all of the actual API network calls for the GCI API happen in the Google Civic Information API Service.

Now for some actual code!

protocol GoogleCivicInfoServiceProtocol {
    func getReps(from url: URL, completion: @escaping (Swift.Result<Reps, Error>) -> Void)
}

As of right now, there is only one function in the service, and the service itself is also defined as a protocol. I won't go into too much Swift-y detail, but learning about it has been an integral part of the journey so I'll include this bit from the Swift documentation, and articulate why it is important. Per the docs:

"A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements. Any type that satisfies the requirements of a protocol is said to conform to that protocol."

The reason I define this service (and the rest) as protocols is so that for testing I can create mock services which also are defined as the same protocols. The difference is, the mock and production services have to have the same functions, but they can be implemented differently. So the mock version of the GCI API service also has a getReps function, but it is coded differently so as not to make actual network calls. Here is the rest of the code for the GCI API service:

class GoogleCivicInfoService: GoogleCivicInfoServiceProtocol {

    private var googleAPIKey: String {
        get {return ProcessInfo.processInfo.environment["Google_API_Key"]!}
    }

    func getReps(from url: URL, completion: @escaping (Swift.Result<Reps, Error>) -> Void) {

        URLSession.shared.dataTask(with: url) { (data, _, error) in
            if let error = error {
                completion(.failure(error))
                return
            }

            guard let data = data else {
                completion(.failure(error!))
                return
            }

            do {
                let lawmakers = try JSONDecoder().decode(Reps.self, from: data)
                completion(.success(lawmakers))
            } catch {
                completion(.failure(error))
            }
        }
        .resume()
    }
}

This is pretty straightforward (and old now) Swift code that executes async network calls to the API. The URL is passed to the service, which then makes the call and then determines if there were any errors. If not, the data is decoded into a Reps object, which is then returned to the ViewModel. Here is an example JSON of what the API returns given a certain address in Florida:

{
  "normalizedInput": {
    "line1": "3275 Northwest 24th Street",
    "city": "Miami",
    "state": "FL",
    "zip": "33142"
  },
  "kind": "civicinfo#representativeInfoResponse",
  "divisions": {
    "ocd-division/country:us/state:fl/cd:26": {
      "name": "Florida's 26th congressional district",
      "officeIndices": [
        1
      ]
    },
    "ocd-division/country:us/state:fl": {
      "name": "Florida",
      "officeIndices": [
        0
      ]
    }
  },
  "offices": [
    {
      "name": "U.S. Senator",
      "divisionId": "ocd-division/country:us/state:fl",
      "levels": [
        "country"
      ],
      "roles": [
        "legislatorUpperBody"
      ],
      "officialIndices": [
        0,
        1
      ]
    },
    {
      "name": "U.S. Representative",
      "divisionId": "ocd-division/country:us/state:fl/cd:26",
      "levels": [
        "country"
      ],
      "roles": [
        "legislatorLowerBody"
      ],
      "officialIndices": [
        2
      ]
    }
  ],
  "officials": [
    {
      "name": "Marco Rubio",
      "address": [
        {
          "line1": "284 Russell Senate Office Building",
          "city": "Washington",
          "state": "DC",
          "zip": "20510"
        }
      ],
      "party": "Republican Party",
      "phones": [
        "(202) 224-3041"
      ],
      "urls": [
        "https://www.rubio.senate.gov/",
        "https://en.wikipedia.org/wiki/Marco_Rubio"
      ],
      "photoUrl": "http://bioguide.congress.gov/bioguide/photo/R/R000595.jpg",
      "channels": [
        {
          "type": "Facebook",
          "id": "SenatorMarcoRubio"
        },
        {
          "type": "Twitter",
          "id": "SenRubioPress"
        }
      ]
    },
    {
      "name": "Rick Scott",
      "address": [
        {
          "line1": "502 Hart Senate Office Building",
          "city": "Washington",
          "state": "DC",
          "zip": "20510"
        }
      ],
      "party": "Republican Party",
      "phones": [
        "(202) 224-5274"
      ],
      "urls": [
        "https://www.rickscott.senate.gov/",
        "https://en.wikipedia.org/wiki/Rick_Scott"
      ],
      "photoUrl": "http://www.flgov.com/wp-content/uploads/2013/05/GovernorNEW-682x1024.jpg",
      "emails": [
        "Rick.scott@eog.myflorida.com"
      ],
      "channels": [
        {
          "type": "Facebook",
          "id": "RickScottSenOffice"
        },
        {
          "type": "Twitter",
          "id": "SenRickScott"
        }
      ]
    },
    {
      "name": "Mario Diaz-Balart",
      "address": [
        {
          "line1": "374",
          "line2": "27 Independence Avenue Southeast",
          "city": "Washington",
          "state": "DC",
          "zip": "20515"
        }
      ],
      "party": "Republican Party",
      "phones": [
        "(202) 225-4211"
      ],
      "urls": [
        "https://mariodiazbalart.house.gov/",
        "https://en.wikipedia.org/wiki/Mario_D%C3%ADaz-Balart"
      ],
      "channels": [
        {
          "type": "Facebook",
          "id": "mdiazbalart"
        },
        {
          "type": "Twitter",
          "id": "MarioDB"
        }
      ]
    }
  ]
}

As you can see, there is quite a lot of data to be fetched with this API. It is effectively the entrypoint for the app and gathers a lot of the initial data necessary for the app to work.

It is also important to note that this isn't the only way you could receive the data. In the use case of LegisLink, where I opted to look for federal congresspeople, I only provided "legislatorUpperBody" and "legislatorLowerBody" in the API call. If you wanted, you could retrieve a number of local, state and more federal officials.

The last bit of code I will share here is how I accomplish this in the ViewModel.

    func getRepsFromGoogleCivicAPIService(googleCivicInfoURL: URL) {
        googleCivicInfoService.getReps(from: googleCivicInfoURL) { [weak self] result in
            switch result {
            case .success(let lawmakers):
                self!.senatorOne = lawmakers.officials[0]

                if (self!.senatorOne.photoURL == nil) {
                    self!.senatorOne.photoURL = "photoURL"
                }

                self!.senatorTwo = lawmakers.officials[1]

                if (self!.senatorTwo.photoURL == nil) {
                    self!.senatorTwo.photoURL = "photoURL"
                }

                self!.representative = lawmakers.officials[2]

                if (self!.representative.photoURL == nil) {
                    self!.representative.photoURL = "photoURL"
                }
            case .failure(let error):
                print("Unexpected error: \(error).")
            }
        }
    }

The ViewModel has a getRepsFromGoogleCivicAPIService function which receives a constructed URL and then passes it the googleCivicInfoService object shown earlier. The results are then passed back into this function and the lawmakers created from the data fetched by the API are then initialized and are usable by the ViewModel.

Final Thoughts

It is a funny feeling reviewing some of the first lines of code I wrote for the app. Looking back, I can already think of a myriad of improvements I could make. That being said, I'm quite proud of how far my understanding of Swift, MVVM principles, and async programming since then. In a future blog I may cover testing this service or maybe even a refactor of some of the code.

Until next time friends!