Nuxt3 on Lambda
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.
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