Using the Google Maps Node.js Client With GraphQL
In this lesson, we'll begin to write the code in our server project to help allow a user to search for listings in a certain location. To achieve this, we'll first modify the existing `listings` GraphQL query field to accept an optional `location` argument that when provided will return the listings that pertain only to that location.
Get the project source code below, and follow along with the lesson material.
Download Project Source CodeTo set up the project on your local machine, please follow the directions provided in the README.md
file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.
This lesson preview is part of the TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two course and can be unlocked immediately with a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two, plus 70+ \newline books, guides and courses with the \newline Pro subscription.
[00:00 - 00:18] In this lesson, we'll begin to write the code necessary to allow the user to search for listings in a certain location. We're going to modify the existing listings GraphQL field to accept an optional location argument that when provided, we'll return listings that pertain only to that location.
[00:19 - 00:53] To access Google's geocoding API, we're going to use the official Node.js client for Google Maps services, which provides access to a list of different Maps APIs, such as the Directions API, Elevation API, and the one we're interested in, the Geocoding API. There isn't any static typing associated with this particular library, but the ReadMe does highlight the following community-prepared typings can be found from the definitely typed repository, under Google_Maps, and note the double_.
[00:54 - 01:22] With that said, in the terminal of our Node server, let's first install the Google Maps client, npm install@google/maps. And once installed, we'll install the community-prepared typings from @types, Google_ms.
[01:23 - 01:47] With the Google Maps node client available, we can begin to add the geocoding functionality. We already have an API Google file in the lib folder, where we've instantiated our OAuth2 client and created the function necessary to log a user in, or in other words, obtain the necessary OAuth token for a logged in user.
[01:48 - 02:11] We'll use this same file to construct a new Maps client constructor, as well as a function-labeled geocode, where our listings resolver will call while passing a location to then obtain geographic information about that location. First, we'll create our Maps client under our constant, we'll simply call Maps near the top of the file.
[02:12 - 02:30] And to create this client, we'll need to run the CreateClient function available to us from the Google Maps package. We'll be prompted to provide an argument.
[02:31 - 02:46] This argument is to be an options object, where at the very minimum, we'll need to supply the API key for our geocoding API. We've already created the geocode key environment variable in our server project about two lessons prior.
[02:47 - 03:06] So in our CreateClient constructor options, we'll specify a key with the value of the geocode key environment variable, with which we can access with process.env. That documentation tells us to make our solution a promise-based solution with which we'll want.
[03:07 - 03:22] We can provide another key value in the options object and place just promise. Now, note, the key it expects here is promise, while the value we're passing in is the actual promise constructor itself.
[03:23 - 03:37] With ES6, we can just simply remove the duplicate renaming of the key and value and just write promise once. With our MapsClient constructed, we'll create the geocode function property of the Google object contained in this file.
[03:38 - 03:56] And this particular function will be called from our GraphQL resolver. For us to actually run the geocoding functionality and provide additional geographic information, like a city, country, address, etc., we'll need access to the location that's been provided from the client.
[03:57 - 04:21] So with that said, we'll assume that location is going to be passed into this function as a string and we'll call the parameter address. With the address to be passed in and the MapsClient constructed, we can run the geocode function available as part of the MapsClient constructor, which accepts an options object that can contain an address.
[04:22 - 04:46] And to maintain a promise-based solution, the documentation tells us to add an as-promise function to this as well. This response is the geocoding response from the API, and it's the response that is to have the address components array, the geometry information, etc.
[04:47 - 05:05] If this response was to ever fail, it could have a status code of either less than 200 or greater than 299. So we can check for this and throw an error if ever to occur.
[05:06 - 05:24] If the response is successful, remember how we've mentioned in the previous lesson how we're interested in parsing the address components array of the response to obtain a country, admin, and city for our query. So we'll attempt to do this functionality pretty much here.
[05:25 - 05:44] What we'll do is we'll call another function we'll create called parse address that will hold the responsibility to parse the address components the way we want to. And we'll pass the address components array from the results with which we can access from the JSON field from the response and the results array from that.
[05:45 - 05:59] And the first object from which is the only object within the results array that has the information we're looking for and the address components. We'll now create the parse address function and we'll create it above our Google objects.
[06:00 - 06:14] We'll state that it is to accept a parameter called address components. We can define the shape of this parameter with an interface we can import from the typings of our Google Maps library called address components.
[06:15 - 06:34] The address component interface represents the shape of a single address component with which we'll say address components is to be an array of these objects. In this function we'll initialize three variables country, admin, and city as no.
[06:35 - 06:54] And by the end of this function we'll return an object that is to contain these three properties. What we're going to do before the return statement is look through the address components array and try and find values of the types that we can map to one of these properties, country, admin, or city.
[06:55 - 07:15] To loop through every item in the address component array we can use a for loop and say for every component in address components we'll do something. First we'll check for country. We mentioned in the last lesson we're going to map the address component that has the type labeled country to our country field in our query.
[07:16 - 07:31] Since the types field within a component or address component is an array we can check and see does the component types include country. If so make the value of the country local variable to the long name of this component.
[07:32 - 07:49] Note that each address component has a long name and short name property. We'll take the long name for everything. We've mentioned in the previous lesson as well we're going to try and map the component where the type includes administration area level one to the value of the admin field in our query.
[07:50 - 08:10] So this will look something like this. And lastly for the city we've mentioned that a component contains the locality type or the postal town type the city will be one of these values.
[08:11 - 08:26] And there we have it. When a resolver function is now to call this geocode function within our Google object and to provide an address input when successful it can accept or expect an object where the country, admin, and the other function.
[08:27 - 08:45] The city admin and city of that location is to be returned from the geocoded aspect. Now one thing to keep in mind is since we're using the appropriate typings for the Google Maps Library if we were to type something a little bit incorrect here something along the lines of administrative level.
[08:46 - 08:54] And the one it's actually able to recognize that it has to include one of the appropriate types. So this helps ensure they were providing the correct values.
[08:55 - 09:07] I might have mentioned administration area sometimes or administration level, but in this particular string it's called administrative area level one. And if we mistyped it it will notice that.
[09:08 - 09:22] Now with that said let's now modify the graph QL type definitions for the listings field in our roots query objects. We'll say it can accept a location argument that is of type string but is optional.
[09:23 - 09:36] Optional since this listings field is going to be used elsewhere in our app like the home page where a location isn't going to be passed in. We'll now look to modify the resolver function for this listings field.
[09:37 - 10:01] The first thing we'll do is we'll update the listing args interface we've created in the types file of our resolver's listing folder to state that a location argument may exist which can be a string or no. And in the listings resolver function within our listing resolver's map we'll say location is now an argument that can be passed in.
[10:02 - 10:29] In our resolver function here we'll create an empty query object and we'll pass that along to our MongoDB find method. Our aim would be to check if location exists and if it does we'll populate this query object with the city admin or country information for the location with which we can get by running the geocode function we've created in our Google API file.
[10:30 - 10:52] First let's use TypeScript to help define the shape of what this query constant can be. In the adjacent types file we'll create and export a listings query interface that could potentially have a country, admin and city fields all of being type string.
[10:53 - 11:19] In the listing resolver's map file we'll import the listings query interface and define the shape of the query constant we've set up with it. And in addition we'll import the Google object we have in our lib API file since we'll need it to run the geocode function.
[11:20 - 11:45] Before we run the Mongo find method we'll check if the location argument exists and if it does we'll run the Google geocode method we've established. We'll pass in the location value and we'll look to retrieve the country admin and city properties from the parsed geocoding we've set up.
[11:46 - 11:56] Here's where we'll check the values of what our geocoding was able to return. If city is available we'll set it as the city property of our query object.
[11:57 - 12:18] If admin exists we'll set it as the admin property of our query object and if country exists we'll set it as the country property of our query object. However if country doesn't exist it probably means the search didn't go well since everything you search for should have a country associated with it.
[12:19 - 12:36] So we'll throw an error that says something like no country found if a country wasn't able to be found. And that's mostly it actually. Notice that it hasn't taken us too long to implement this level of location based searching for listings.
[12:37 - 12:48] So let's see how what we've done behaves. We'll head over to GraphQL Playground . We'll attempt to query for the listings field. We'll say we want them ordered from price high to low.
[12:49 - 13:13] We'll specify a limit of 10 and we'll say we want the first page of results and in our results we'll try and get the ID, title, city, admin and country of each listing. Hmm. The playground tells us that the admin and country fields aren't part of the listing GraphQL object.
[13:14 - 13:29] We take a look at our code. We do know that admin and country are part of the TypeScript definitions we've set up for the listing documents in our database. And we've seeded our listing documents with values for the admin and country fields.
[13:30 - 13:44] So we'll need to simply now update the definition of our GraphQL listing object to contain these fields as well. And country and admin will be strings and there will be none null as well.
[13:45 - 13:57] If we head back to GraphQL Playground, refresh the page and look to make the query again, we'll get a bunch of different listings from different locations in the world. Cool.
[13:58 - 14:12] So now let's add a location argument to our query and we'll provide a value of Toronto. When we now run our query, everything we get returned back to us is in Toronto, Canada.
[14:13 - 14:24] Amazing. Okay, how about we provide a location of Los Angeles? We'll get listings only within the Los Angeles area.
[14:25 - 14:46] If we want to go country level and say United States, we'll get the listings for both Los Angeles and San Francisco. If we type the location that we don't have any listings for from our mock data, maybe New York, we'll get an empty result array.
[14:47 - 14:58] Amazing. So far, so good. However, at this moment when the client makes the request, it won't know what the geocoder was able to note as the location.
[14:59 - 15:15] For example, when we searched for United States, we'll want to convey that we 're looking for listings all over the United States. If we searched for Los Angeles, we'll want to convey to the client that, hey, we're looking for listings in Los Angeles, California, United States.
[15:16 - 15:44] To help us here, what we can do is we can return an interpolated string message from the server that simply contains the city, admin and country that was recognized from our geocoder. So, in our GraphQL type definitions, we'll say the listings GraphQL objects, or in other words, the expected return for the listings GraphQL field, will now contain a region field of type string and is to be optional.
[15:45 - 16:08] It's optional because we'll only expect a region to be filled here or to have a value when a location is provided in the arguments. And in the TypeScript types file for our listing resolvers map, we'll update the listings data interface to have a region property of type string or null.
[16:09 - 16:25] If we take a look at the type definitions file again, yeah, we're not supposed to use semicolons here in the schema definition language. So I'm going to remove the semicolon I've added and just simply say region is going to be a string, but it could be null.
[16:26 - 16:47] In the listings resolver function in the listing resolvers map, we'll update the initial value of the data object we set up with a region of null. And finally, if a location exists, we'll try and create an interpolated string that has the city, admin and country information that's been found.
[16:48 - 17:05] City and admin may not always be found from the geocoder. So first, we'll construct text constants for city and admin and we'll state that if those particular values are found, we'll create string values of what they are followed with a comma and a space.
[17:06 - 17:28] And if they're not found, these particular constants will just be blank strings . And we'll state the region property of the data objects will be an interpol ation of the city text that was found, the admin text that was found and the country that has been found.
[17:29 - 18:09] So now what we expect to see here is if both a city, admin and country has all been found, we'll see that this region string within our data object that is to be returned could be something like Los Angeles, California, United States. If country was only found and neither city or admin exists, we'll just see United States. So now let's attempt to make our queries again. We'll head over to GraphQL Play ground and we'll specify that we want to query for the new region field and for the location, we'll say United States.
[18:10 - 18:31] And boom, this tells us that the geocoder now's returned region of just United States and no city or administration was retrieved, which makes sense since there wasn't any. If we searched for Los Angeles or in fact, if we just searched for LA, we'll get Los Angeles, California, United States as our region.
[18:32 - 18:49] Now the spacing and the comma besides the city, admin and country is how we've prepared it so it'll be presentable on the client without any additional formatting. And that's it. Take some time to play around with the solution we've just created.
[18:50 - 19:03] Now we've noticed that it isn't 100% of a perfect solution, but it's pretty darn good for what we intend to do. For example, some very small weird edge cases are if we searched for something like Ireland.
[19:04 - 19:13] It would error out since it can't find a country associated with it. Perhaps it wants us to specify not Northern or Southern Ireland.
[19:14 - 19:27] But if we say Southern Ireland, we'll get the region as Ireland. But honestly, these are just very, very few use cases we found where we're unable to find the appropriate region with a valid location.
[19:28 - 19:47] And if they are to occur, what we've noticed is if you simply rerun the query with a more fine-tuned location, it often does the trick. Amazing. In the next coming lessons, we'll now have our client make the query and show the results for certain locations within the listings page.