Nuxt3 on Lambda

Fabio Gollinucci
5 min readJun 11, 2023

--

Since the Nuxt version 3 the server side engine is managed by Nitro. This engine (based on h3) can handle a variety of environments including AWS Lambda.

AWS Infrastructures

Lambda integration

The integration directly handles the Lambda function event that comes from API Gateway executing Nuxt server side rendering.

HttpApi:
Type: AWS::Serverless::HttpApi

HttpApiFunction:
Type: AWS::Serverless::Function
Properties:
# [...]
Events:
ProxyResource:
Type: HttpApi
Properties:
ApiId: !Ref HttpApi
Path: $default
Method: any

The response will be in HTML format, ready to be rendered by browser. These responses can be cached to avoid too many requests triggering the Lambda function.

The Lambda function handler will be generated by build command in .output/server/index.mjs:

// [...]
export { h as handler } from './chunks/nitro/aws-lambda.mjs';
// [...]

Routing

With Cloudfront origins control where the request will be sent changing TargetOriginId identifier to route requests to S3 bucket or Nuxt SSR:

Origins:
- Id: static
DomainName: !Sub "${Bucket}.s3.${AWS::Region}.amazonaws.com"
S3OriginConfig:
OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${OriginAccessIdentity}
- Id: ssr
DomainName: !Sub "${HttpApi}.execute-api.${AWS::Region}.amazonaws.com"
OriginPath: ""
OriginCustomHeaders:
- HeaderName: X-SSR-Protection
HeaderValue: !Ref OriginAccessIdentity
CustomOriginConfig:
HTTPSPort: 443
OriginProtocolPolicy: https-only
OriginSSLProtocols:
- TLSv1.2

The Nuxt application can also implement API routes, these paths with /api prefixes need to be handled differently. They may need more dynamism and require specific caching configurations.

CacheBehaviors:
# [...]
- PathPattern: /api/*
AllowedMethods:
- GET
- HEAD
- OPTIONS
- PUT
- PATCH
- POST
- DELETE
TargetOriginId: ssr
ViewerProtocolPolicy: redirect-to-https
# [...]

Static built files cannot be served through the SSR implementations and need to be served from a S3 Bucket. Nuxt by default loads these files from the /_nuxt path, this needs to be routed correctly from Cloudfront.

CacheBehaviors:
# [...]
- PathPattern: /_nuxt/*
AllowedMethods:
- GET
- HEAD
- OPTIONS
TargetOriginId: static
ViewerProtocolPolicy: redirect-to-https
# [...]

Also static files served from the root directory, like robots.txt or favicon.ico, need to be routed correctly.

CacheBehaviors:
# [...]
- PathPattern: /favicon.ico
AllowedMethods:
- GET
- HEAD
- OPTIONS
TargetOriginId: static
ViewerProtocolPolicy: redirect-to-https
# [...]
- PathPattern: /robots.txt
AllowedMethods:
- GET
- HEAD
- OPTIONS
TargetOriginId: static
ViewerProtocolPolicy: redirect-to-https
# [...]

If the request’s path does not match any PathPattern configurations the default one will be used:

DefaultCacheBehavior:
AllowedMethods:
- GET
- HEAD
- OPTIONS
TargetOriginId: ssr
ViewerProtocolPolicy: redirect-to-https

This indicate the all navigation requests (/, /category/1, /who-we-are..) will be sent to Nuxt for server side rendering the page.

The API gateway that handle the Nuxt SSR requests will call the Lambda function for every method or path:

HttpApiFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Ref AWS::StackName
# [...]
Events:
ProxyResource:
Type: HttpApi
Properties:
ApiId: !Ref HttpApi
Path: $default
Method: any

Caching

Caching strategy can differ from one path to an other, this can be easily configured with AWS managed cache policies:

- PathPattern: /api/*
# [...]
CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingDisabled
- PathPattern: /_nuxt/*
# [...]
CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # CachingOptimized

Or creating your own specific configurations:

CachePolicy15Min:
Type: AWS::CloudFront::CachePolicy
Properties:
CachePolicyConfig:
Comment: Cache objects for 15 minutes
DefaultTTL: 900
MinTTL: 900
MaxTTL: 86400
Name: !Sub "${AWS::StackName}-15min"
ParametersInCacheKeyAndForwardedToOrigin:
EnableAcceptEncodingGzip: true
EnableAcceptEncodingBrotli: true
HeadersConfig:
HeaderBehavior: none
QueryStringsConfig:
QueryStringBehavior: none
CookiesConfig:
CookieBehavior: whitelist
Cookies:
- 'language'

Distribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
# [...]
CacheBehaviors:
- PathPattern: /api/*
# [...]
CachePolicyId: !Ref CachePolicy15Min

The path pattern can be more specific changing cache behavior for different APIs:

- PathPattern: /api/products/*
# [...]
CachePolicyId: !Ref CachePolicy15Min
- PathPattern: /api/config/*
# [...]
CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # CachingOptimized
- PathPattern: /api/*
# [...]
CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # CachingDisabled

Remember that is simple to add cache, the real problem is invalidate it when the request need to respond with a fresh new data. This can be archived with the Cloudfront CreateInvalidation API.

Security

Using AWS managed response headers policies (or creating your own response header policy) Cloudfront will modify the response headers before sending them to the clients adding or removing headers.

DefaultCacheBehavior:
# [..]
ResponseHeadersPolicyId: 67f7725c-6f97-4210-82d7-5512b31e9d03 # SecurityHeadersPolicy

AWS Services integration

On server side Nuxt can use AWS SDK to interact with AWS services. The API authentication can be handled locally using environment variables and/or profile configuration.

This is a simple implementation of useAwsConfig server utility that encapsulate the logic for AWS SDK clients constructor settings:

// server/utils/useAwsConfig.ts

import { fromIni, fromEnv } from "@aws-sdk/credential-providers";
import { AwsCredentialIdentityProvider } from "@aws-sdk/types";

interface AwsConfig {
region: string
credentials: AwsCredentialIdentityProvider
}

let config: AwsConfig
export default function(): AwsConfig {
if (!config) {
const runtimeConfig = useRuntimeConfig()
const profile = runtimeConfig.aws?.profile || process.env.AWS_PROFILE || process.env.AWS_DEFAULT_PROFILE;
config = {
region: runtimeConfig.aws?.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION,
credentials: profile
? fromIni({
profile: profile,
})
: fromEnv()
}
}
return config
}

Runtime configurations can be stored in Nuxt config file in order to centrally manage AWS services configurations:

// nuxt.config.ts

export default defineNuxtConfig({
runtimeConfig: {
aws: {
region: 'eu-west-1',
profile: 'default'
},
dynamodb: {
tableName: process.env.DYNAMODB_TABLE,
},
sqs: {
queueUrl: process.env.SQS_QUEUE_URL,
},
events: {
busName: process.env.EVENT_BRIDGE_BUS,
}
}
})

Any other AWS SDK clients can use this utility to retrieve region and authentication configuration. This is an example of useDynamoDB server utility that return the DynamoDB document client:

// server/utils/useDynamoDB.ts

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";

interface DynamoDBConfig {
client: DynamoDBDocumentClient
tableName: string
}

let config: DynamoDBConfig
export default function(): DynamoDBConfig {
if (!config) {
const runtimeConfig = useRuntimeConfig()
// return both client and table name
config = {
client: DynamoDBDocumentClient.from(
new DynamoDBClient(useAwsConfig())
),
tableName: runtimeConfig.dynamodb?.tableName || process.env.DYNAMODB_TABLE
}
}
return config
}

These client utilities will help to interact with services from server APIs:

// server/api/db/put.post.ts

import { randomUUID } from "crypto";
import { PutCommand } from "@aws-sdk/lib-dynamodb";

export default defineEventHandler(async (event) => {
// get authenticated DynamoDB document client
const { client, tableName } = useDynamoDB()

const body = await readBody(event)
body.id = randomUUID()

// execute AWS APIs using AWS SDK
await client.send(new PutCommand({
TableName: tableName,
Item: body
}))

return {
id: body.id
}
})

When using the AWS APIs within a Lambda, you must also declare the policies that must be connected to the role of the function:

HttpApiFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Ref AWS::StackName
# [...]
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref DynamoDBTable
- SQSSendMessagePolicy:
QueueName: !GetAtt SQSQueue.QueueName
- EventBridgePutEventsPolicy:
EventBusName: !Ref EventBridgeBus

Deployment

When running build command set the Nitro provider to “aws-lambda":

NITRO_PRESET=aws-lambda npm run build

Nuxt built static files located in .output/public need to be deployed to the S3 bucket.

aws s3 cp --recursive .output/public s3://<bucket name>/

The Nuxt SSR built code that will run into the Lambda function environment can be deployed with SAM. The source code is located in “.output/server":

HttpApiFunction:
Type: AWS::Serverless::Function
Properties:
# [...]
CodeUri: .output/server/

Lambda function will be packed and deployed during SAM deploy:

sam deploy

When deploy procedures end the Cloudfront cache may need to be invalidated:

aws cloudfront create-invalidation --distribution-id <distribution id> --paths '/*'

Repository: daaru00/nuxt3-lambda

Credits: Cloudcraft

--

--