Simple GraphQL API in AWS Lambda
I am building a simple app. The internal APIs which the app uses are all GraphQL endpoints. What's remaining is an API to look up location info of an IP address. I've found this free API at https://ip-api.com/docs, but it doesn't have a GraphQL endpoint. I don't want my app to support heterogeneous API call stacks, nor do I want to include additional libraries because of different API stacks.
Continuing my journey of taking advantage of free cloud computing resources, I am going to build a GraphQL server adapter and deploy it in AWS Lambda. Lambda offers one million function calls for free each month!
Dependencies
I'll use Node. Dependencies are:
"dependencies": { "aws-serverless-express": "^3.3.8", "axios": "^0.19.2", "cors": "^2.8.5", "express": "^4.17.1", "express-graphql": "^0.9.0", "graphql": "^15.1.0" }
Typical express server, with graphql module and hooked using express-graphql. axios is used to make outbound API calls. cors is there because my web app will be hosted on a different domain than AWS Lambda. aws-serverless-express proxies an express server request and response to Lambda.
Schema
Since this is a simple adapter, I'll use the original ip-api.com JSON response fields as-is. I'll create a lookup query which takes one argument called query (the IP address to be looked up). As an adapter, the resolver for the query simply calls the real ip-api.com API and graphql module formats the response.
// schema.js const axios = require('axios'); const { GraphQLObjectType, GraphQLInt, GraphQLString, GraphQLBoolean, GraphQLList, GraphQLSchema } = require('graphql'); const IPGeoType = new GraphQLObjectType({ name: 'IPGeo', fields: () => ({ continent: { type: GraphQLString }, continentCode: { type: GraphQLString }, country: { type: GraphQLString }, countryCode: { type: GraphQLString}, region: { type: GraphQLString }, regionName: { type: GraphQLString }, city: { type: GraphQLString }, district: { type: GraphQLString }, zip: { type: GraphQLString }, lat: { type: GraphQLString }, lon: { type: GraphQLString }, timezone: { type: GraphQLString }, offset: { type: GraphQLInt }, currency: { type: GraphQLString }, isp: { type: GraphQLString }, org: { type: GraphQLString }, as: { type: GraphQLString }, asname: { type: GraphQLString }, mobile: { type: GraphQLBoolean }, proxy: { type: GraphQLBoolean }, hosting: { type: GraphQLBoolean }, query: { type: GraphQLString } }) }); // Root query const RootQuery = new GraphQLObjectType({ name: 'RootQueryType', fields: { lookup: { type: IPGeoType, args: { query: { type: GraphQLString } }, resolve(parent, args) { return axios.get(`https://ip-api.com/json/${args.query}?fields=status,message,continent,continentCode,country,countryCode,region,regionName,city,district,zip,lat,lon,timezone,offset,currency,isp,org,as,asname,mobile,proxy,hosting,query`) .then(res => res.data); } } } }); module.exports = new GraphQLSchema({ query: RootQuery
});
App
The main entry point (app.js) is simply an express server. The handler is express-graphql's graphHTTP. I pass in my schema above, and enable graphiql (a web tool to compose and make GraphQL calls) for convenience of debugging.
// app.js const express = require('express'); const graphqlHTTP = require('express-graphql'); const cors = require('cors'); const schema = require('./schema.js'); const app = express(); app.use(cors()); app.use('/', graphqlHTTP({ schema: schema, graphiql: true }));
module.exports = app;
Local and Lambda Deployments
In order to test before going to Lambda, I have server.js to start a dev server locally. For Lambda deployment, it will use index.js (as we will see later).
//server.js for local const app = require('./app'); const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`server started on port ${PORT}`));
and
// index.js for Lambda const awsServerlessExpress = require('aws-serverless-express'); const app = require('./app'); const server = awsServerlessExpress.createServer(app);
exports.handler = (event, context) => awsServerlessExpress.proxy(server, event, context);
That's all the coding! The complete source code is on github.
I can run "npm install && npm run server" to bring up dev server locally, and hit https://localhost:5000 to test.
Lambda
First, I'll create a new Lambda function. From AWS Lambda service home screen, click on Create function button, then choose "Author from scratch" option.
Give it any name, eg., "ipgeo".
Leave the default Runtime to the latest version of Node.js.
Under permissions, "create a new role with basic Lambda permissions".
Click "Create function".
By default, Lambda lets you author code in browser under Function code section. Because I have dependency modules, I'll need to upload a zip of my local code. Note that I need to "npm install" the node_modules before I zip up my files. The zip root should be the folder that contains index.js.
Drop down Actions button beside "Fucntion code" and choose to "Upload a .zip file".
After uploading the zip, the code authoring section looks something like this:
AWS API Gateway
In order to expose the API, I make use of AWS API Gateway. Okay, I lied. API Gateway isn't free. It cost $1.00 for 1 million HTTP API calls. I can't find out how to do it completely free in AWS. If you know, please help me save $1.00.
Scroll up on the Lambda page, find the Add Trigger button, add API Gateway as trigger.
Choose "Create an API", and "HTTP API", Change Security to "Open", click "Add".
After an API Gateway is successfully added, I am brought back to the Lambda page. API Gateway is shown hooked up with ipgeo Lambda function.
Expand API Gateway Details, I can get the API endpoint URL.
Click on the URL, GraphiQL interface shows up like this:
Yes!! It's successfully deployed! Let's test it out.
Testing the Lambda API
On the left hand side, enter a lookup query for an IP address:
{ lookup(query: "213.12.222.22") { continent continentCode country countryCode region regionName city district zip lat lon timezone offset currency isp org as asname mobile proxy hosting query }
}
Ctrl-Enter to run the query, you should get the IP location info back.
{ "data": { "lookup": { "continent": "Europe", "continentCode": "EU", "country": "United Kingdom", "countryCode": "GB", "region": "ENG", "regionName": "England", "city": "London", "district": "", "zip": "W14 0UF", "lat": "51.4942", "lon": "-0.214694", "timezone": "Europe/London", "offset": 3600, "currency": "GBP", "isp": "EDEX", "org": "Cable & Wireless UK P.U.C.", "as": "", "asname": "", "mobile": false, "proxy": false, "hosting": false, "query": "213.12.222.22" } }
}
It works!
The beauty of GraphQL is that its schema is exposed to the endpoint, so GraphiQL is aware of the schema and and prompt for the fields. Use Ctrl-space for the tool to suggest what fields to use.
And, the client can choose which fields the API responds with. For example, if the client is only interested in lat and lon of an IP address, the query can specify:
{ lookup (query: "123.123.123.123") { lat lon }
}
That's it for now. Have fun!
Cloud Support Engineer / Web Developer
3 年This article helped me a lot to understand what is what and how they work with each other. Many thanks to you, Kenny! ??
Helping developers experience the future now
4 年Nice walk through Kenny.