Creating a Lambda Function#
Initialize a Lambda function using Cargo Lambda. Create a new function with the following command.
cargo lambda new {project-name}
Since we're creating an HTTP API, answer the questions with y.
Local Testing#
In the created directory, run the following command.
cargo lambda watch
This command starts a local server for your Lambda function. It supports Lambda's Function URL feature, allowing you to test your GraphQL API locally. Access localhost:9000
to verify that you receive a proper response.
Creating a GraphQL API Server#
Add the necessary crates.
cargo add async-graphql
cargo add serde --features derive
cargo add serde_json
Displaying the Playground#
Before creating the GraphQL API, let's set up a playground to make testing easier.
The following code displays the playground (GraphiQL) when receiving a GET request.
use async_graphql::http::GraphiQLSource;
use lambda_http::{run, service_fn, tracing, Body, Error, Request, Response};
use serde_json::json;
async fn function_handler(event: Request) -> Result<Response<Body>, Error> {
if event.method() == Method::GET {
let playground_html = GraphiQLSource::build().finish();
let response = Response::builder()
.status(200)
.header("content-type", "text/html")
.body(playground_html.into())
.map_err(Box::new)?;
Ok(response)
} else if event.method() Method::POST {
todo!("GraphQL API");
} else {
let response = Response::builder()
.status(405)
.header("content-type", "application/json")
.body(json!({"error":"Method Not Allowed"}).to_string().into())
.map_err(Box::new)?;
Ok(response)
}
}
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing::init_default_subscriber();
run(service_fn(function_handler)).await
}
After running the command below, access localhost:9000
in your browser to confirm that the GraphiQL Playground appears.
cargo lambda watch
Implementing the GraphQL API#
The GraphQL API processes all requests through a single endpoint using the POST method.
Create new files. Set up the query root in query.rs and store resolvers under the
resolvers/
directory.
src/
├── main.rs
├── query.rs
├── resolvers
│ └── greet.rs
└── resolvers.rs
Create a query resolver. Initially, it will only return constants. Simple resolvers can be implemented with SimpleObject, but we'll use the regular
Object attribute for this example.
pub struct Greet {
pub message: String,
pub language: String,
}
impl Greet {
pub fn new() -> Result<Self, async_graphql::Error> {
Ok(Greet {
message: "Hello, GraphQL!".to_string(),
language: "Rust".to_string(),
})
}
}
#[async_graphql::Object]
impl Greet {
/// A delightful message from the server
pub async fn message(&self) -> String {
self.message.to_string()
}
/// Languages that implement GraphQL
pub async fn language(&self) -> String {
self.language.to_string()
}
}
Register the resolver to the query root.
pub mod greet;
use async_graphql::*;
pub struct QueryRoot;
use crate::resolvers;
#[async_graphql::Object]
impl QueryRoot {
/// Returns a greeting message along with the programming language.
pub async fn greet(&self) -> Result<resolvers::greet::Greet, async_graphql::Error> {
resolvers::greet::Greet::new()
}
}
Configure the entry point to handle GraphQL requests. This is the block that branches on the POST method in the code below.
use async_graphql::{http::GraphiQLSource, EmptyMutation, EmptySubscription, Schema};
use lambda_http::{run, service_fn, tracing, Body, Error, Request, Response};
use serde_json::json;
mod query;
mod resolvers;
async fn function_handler(event: Request) -> Result<Response<Body>, Error> {
let schema = Schema::build(query::QueryRoot, EmptyMutation, EmptySubscription).finish();
if event.method() == Method::GET {
let playground_html = GraphiQLSource::build().finish();
let response = Response::builder()
.status(200)
.header("content-type", "text/html")
.body(playground_html.into())
.map_err(Box::new)?;
Ok(response)
} else if event.method() == Method::POST {
let request_body = event.body();
let gql_request = match serde_json::from_slice::<async_graphql::Request>(request_body) {
Ok(request) => request,
Err(err) => {
return Ok(Response::builder()
.status(400)
.header("content-type", "application/json")
.body(
json!({"error": format!("Invalid request body: {}", err)})
.to_string()
.into(),
)
.map_err(Box::new)?);
}
};
let gql_response = schema.execute(gql_request).await;
let response_body = match serde_json::to_string(&gql_response) {
Ok(body) => body,
Err(err) => {
return Ok(Response::builder()
.status(500)
.header("content-type", "application/json")
.body(
json!({"error": format!("Failed to serialize response: {}", err)})
.to_string()
.into(),
)
.map_err(Box::new)?);
}
};
Ok(Response::builder()
.status(200)
.header("content-type", "application/json")
.body(response_body.into())
.map_err(Box::new)?)
} else {
let response = Response::builder()
.status(405)
.header("content-type", "application/json")
.body(json!({"error":"Method Not Allowed"}).to_string().into())
.map_err(Box::new)?;
Ok(response)
}
}
#[tokio::main]
async fn main() -> Result<(), Error> {
tracing::init_default_subscriber();
run(service_fn(function_handler)).await
}
Verify that queries work correctly in the playground.
- Adding Context
Deploying the Lambda Function#
For this example, we'll deploy the Lambda function using AWS CDK.
Building the Lambda Function#
The following command generates an executable binary in the target/lambda/{crate-name}/
directory.
cargo lambda build --release
The bootstrap
file in this directory serves as the entry point. We'll deploy this file.
Initializing a CDK Project#
We'll use AWS CDK to deploy the Lambda function. Initialize a CDK project.
mkdir cdk
cd cdk
cdk init app --language typescript
Creating a Lambda Stack#
First, let's deploy using Lambda's Function URL feature. Create a stack like this:
import * as cdk from 'aws-cdk-lib'
import { Construct } from 'constructs'
import * as lambda from 'aws-cdk-lib/aws-lambda'
import * as path from 'path'
export class LambdaStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props)
const lambdaFunction = new lambda.Function(this, 'LambdaFunction', {
functionName: 'rust-graphql-function-url',
code: lambda.Code.fromAsset(
path.resolve(__dirname, '../../target/lambda/lambda-rust-graphql/')
),
handler: 'main',
runtime: lambda.Runtime.PROVIDED_AL2023
})
const lambdaVersion = new lambda.Version(this, 'LambdaVersion', {
lambda: lambdaFunction
})
const lambdaAlias = new lambda.Alias(this, 'LambdaAlias', {
aliasName: 'latest',
version: lambdaVersion.latestVersion
})
const lambdaFunctionURL = new lambda.FunctionUrl(this, 'FunctionURL', {
function: lambdaAlias,
authType: lambda.FunctionUrlAuthType.NONE
})
new cdk.CfnOutput(this, 'FunctionURLOutput', {
value: lambdaFunctionURL.url
})
}
}
Import the stack in the CDK entry point and create an instance.
#!/usr/bin/env node
import 'source-map-support/register'
import * as cdk from 'aws-cdk-lib'
import { LambdaStack } from '../lib/lambda'
const app = new cdk.App()
new LambdaStack(app, 'Lambda', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: process.env.CDK_DEFAULT_REGION
}
})
Deploying the Lambda Stack#
Try deploying the function with the following command. The Lambda Function URL should be output as a CloudFormation Output in the standard output.
cdk deploy Lambda [--profile]
The Function URL can also be checked from the management console. (Note that it doesn't appear on the default screen since we're using an alias)
If you access this URL (https://*.lambda-url.ap-northeast-1.on.aws
) and the playground appears, you've succeeded! Try running queries just like you did locally.
- Bonus: Deployment with API Gateway
Code examples can be found here.