Ready to take your Vue development to the next level with Nuxt? Great. But there’s one catch.

Nuxt’s default deployment mode is as a continuously running Node.js server. For those of us who enjoy the cost-efficiency of serverless tools like AWS Lambda, this setup isn’t ideal.

The good news? It is possible to configure Nuxt as a serverless application.

In this guide, you’ll discover how to deploy Nuxt to AWS Lambda using a simple Serverless Framework configuration file. Follow along to make sure Nuxt does its server-side thing without paying for 24/7 compute.

Adding a hint of Nuxt to your Vue app

Chances are you already followed the path to your first HelloWorld Vue app and experienced client-side utopia.

However advanced your Vue skills, you may have noticed some downsides of the single-page applications created by this framework:

  1. Browsers generate every HTML element, possibly slower than on the server.
  2. It’s difficult to set simple metadata like the page title.
  3. Search engines apply SEO penalties to websites rendered in-browser.

To address Vue’s shortcomings, Nuxt steps in to save the day.

Nuxt is a Vue++ framework that straddles the gap between client and server. It intelligently renders the non-dynamic parts of a webpage on the server, leaving the dynamic stuff to the browser.

That means faster webpages and so much SEO juice that Google is bound to send loads of traffic to your newly optimised site.

But before building and deploying a Nuxt application to AWS, let’s first understand how Nuxt actually works.

Nuxt server and client-side magic

Nuxt adds functionality to make Vue development easier (less code) and more powerful (extra features).

Official documentation describes how to use these goodies locally. However, to successfully deploy a Nuxt application to AWS, we must understand its architecture.

So what makes up a running Nuxt application?

  1. Backend code: responds to requests from the browser by generating HTML.
  2. Static files: JavaScript, CSS, and other resources the browser downloads.

Yes, it’s rather simple.

When you request a Nuxt application in the browser, such as https://nuxt.tomgregory.com/, you call a backend service that returns an HTML response. The browser renders that HTML, which requests static files such as JavaScript and CSS from URLs beginning https://nuxt.tomgregory.com/_nuxt/.

So it works like any dynamic website then?

Well, yes. But the clever thing about Nuxt is it knows what HTML to generate on the backend vs. the frontend. Nuxt renders HTML that never changes on the server, leaving the browser to render HTML that does change.

How does it do that?

It’s all in Nuxt’s build process.

Running nuxt build generates an .output directory with two subdirectories:

  1. .output/server: JavaScript code that runs on the backend and generates HTML.
  2. .output/public: JS, CSS, and static files that must be available to the browser.

Deploying a Nuxt application is just a question of figuring out how to deploy the server and public build outputs.

Unfortunately, when you run nuxt build, the generated server code runs on a Node.js server—the kind that runs all day even when your site has low traffic. Obviously that’s not compatible with our preferred environment, AWS Lambda.

Fortunately, Nuxt has another trick up its sleeve.

The Nitro preset that makes everything work

The next piece of the Nuxt puzzle is Nitro, a server toolkit that comes bundled with Nuxt.

Think of it as the glue between the Nuxt framework and a target deployment environment. Nitro deploys to any of 24 environments by configuring a provider.

Can you guess which is the default provider? Yep, Node.js. Sigh!

Of course, anyone who’s used AWS Lambda knows it has a Node.js runtime. But it requires a handler that follows a specific format, like this:

export const handler = async (event) => {
    return {
        statusCode: 200,
        body: "Amazing! Your Lambda function is working.",
        headers: {
            "content-type": "text/plain"
        }
    }
}

This code:

  • Defines a handler function.
  • Returns an HTTP response JSON object to AWS API Gateway.

To package a Nuxt application in the right Lambda format, use the aws_lambda provider.

Just add the snippet highlighted below to your application’s nuxt.config.ts:

export default defineNuxtConfig({
  compatibilityDate: '2024-04-03',
  devtools: { enabled: true },
  nitro: {
    preset: "aws_lambda",
  }
})

With this in place, nuxt build generates the required server code.

So run npm run build, then in .output/server/index.mjs you should see the word handler among some other code. Your application is Lambda-compatible!

Now we just need a way to deploy a function to AWS with minimal configuration.

Deploy to AWS with Serverless Framework

Serverless Framework is the deployment tool that requires the least code to define a Lambda function.

It uses a serverless.yml configuration file, which specifies the AWS resources to create/update during deployment.

I’m assuming you already have a project setup with Serverless Framework. If not, run npm i serverless -g, add a serverless.yml file to your project, and we’ll start cooking.

Let’s use Serverless Framework to define:

  • An AWS Lambda function which uses the server code in .output/server.
  • An S3 bucket to which we’ll publish static files from .output/public.
  • Other AWS resources needed to access a Nuxt application over the internet.

Let’s step through each section of the serverless.yml file, then at the end I’ll hook you up with the complete file.

1. AWS Lambda function

This defines the Lambda function that will run Nuxt code to generate HTML and do other magic.

The following template includes:

  1. Deployment options (provider)
  2. Which files to package with our function (package)
  3. The function itself (functions)
service: nuxt-lambda-demo
frameworkVersion: '4'
provider:
  name: aws
  runtime: nodejs18.x
  region: us-east-1
  deploymentMethod: direct
package:
  patterns:
    - '!**'
    - '.output/server/**'
functions:
  nuxtSSREngine:
    handler: .output/server/index.handler
    events:
      - httpApi:
          path: /{proxy+}
          method: get

The most relevant part is under nuxtSSREngine which:

  • Creates a Lambda function using the index.mjs file from the nuxt build step.
  • Integrates the function with an HTTP API for any GET request.

This HTTP API is an AWS API Gateway resource that Serverless Framework creates automatically. You can hook functions into it so they’re exposed on the public internet.

S3 bucket

The S3 bucket is cloud storage for hosting static files (JS, CSS, etc.). The client-side part of our application fetches these once the HTML is returned from the Lambda function.

params:
  default:
    staticResourceBucketName: ${self:service}-static-resources
resources:
  Resources:
    StaticResourceBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${param:staticResourceBucketName}
        PublicAccessBlockConfiguration:
          BlockPublicAcls: true
          BlockPublicPolicy: true
          IgnorePublicAcls: true
          RestrictPublicBuckets: true

This S3 bucket:

  • Is completely private (later we’ll make it accessible to CloudFront).
  • Uses a bucket name from a parameter. That could be very handy if we ever need to reference it elsewhere.

CloudFront distribution

CloudFront is the entry point to the back-end part of our application. Based on the URL’s path, it routes requests to various services.

In our case, we want to:

  • Route requests for static resources to the S3 bucket.
  • Route any other requests to the HTTP API backed by the Lambda function.

The following configuration creates such a CloudFront distribution.

    CloudFrontDistribution:
      Type: AWS::CloudFront::Distribution
      Properties:
        DistributionConfig:
          Enabled: true
          DefaultCacheBehavior:
            TargetOriginId: "nuxt-ssr-engine"
            ViewerProtocolPolicy: redirect-to-https
            CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # AWS Managed Cache Policy(CachingDisabled)
          CacheBehaviors:
            - PathPattern: "/static/*"
              TargetOriginId: "nuxt-static-resources"
              ViewerProtocolPolicy: redirect-to-https
              CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # AWS Managed Cache Policy(CachingDisabled)
          Origins:
            - Id: "nuxt-static-resources"
              DomainName: !GetAtt StaticResourceBucket.RegionalDomainName
              OriginAccessControlId: !GetAtt StaticResourceOriginAccessControl.Id
              S3OriginConfig: {}
            - Id: "nuxt-ssr-engine"
              DomainName: !Select [1, !Split ["//", !GetAtt HttpApi.ApiEndpoint]]
              CustomOriginConfig:
                OriginProtocolPolicy: https-only

Things to note:

  • The default behaviour is to call nuxt-ssr-engine i.e. our Lambda function that generates HTML.
  • Requests to /static access nuxt-static-resources i.e. static resources in our S3 bucket.

This CloudFront distribution will only work if requests for static resources are prefixed with the path /static.

How can we achieve that?

Fortunately Nuxt exposes a configuration to load static resources from any URL.

Nuxt CDN URL

Setting cdnURL in nuxt.config.js makes Nuxt look for static resources wherever you choose.

That’s good, but better to dynamically generate the property, since it contains the CloudFront domain name.

You can set cdnURL via a NUXT_APP_CDN_URL environment variable. Just configure this Lambda function environment variable in serverless.yml.

functions:
  nuxtSSREngine:
    handler: .output/server/index.handler
    events:
      - httpApi:
          path: /{proxy+}
          method: get
    environment:
      NUXT_APP_CDN_URL: !Sub "https://${param:domainName}/static/"

Now any HTML that Nuxt generates will reference static resource with this URL prefix.

Important: we must make sure to copy static resources into a directory called static in S3, otherwise CloudFront won’t be able to access them.

S3 permissions

To give CloudFront access to S3, follow least-privilege security principles with these final two resources.

    StaticResourceOriginAccessControl:
      Type: AWS::CloudFront::OriginAccessControl
      Properties:
        OriginAccessControlConfig:
          Name: ${self:service}-origin-access-control
          OriginAccessControlOriginType: s3
          SigningBehavior: always
          SigningProtocol: sigv4
    StaticResourceBucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket: !Ref StaticResourceBucket
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
            Sid: AllowCloudFrontServicePrincipalReadOnly
            Effect: Allow
            Principal:
              Service: cloudfront.amazonaws.com
            Action: s3:GetObject
            Resource: !Sub "arn:aws:s3:::${StaticResourceBucket}/*"
            Condition:
              StringEquals:
                AWS:SourceArn: !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}"

This ensures CloudFront is the only AWS resource with access to the S3 bucket.


Deploy the whole template to AWS with the command sls deploy. You can reference the full serverless.yml file provided at the end.

After what feels like forever as AWS does its thing, open the AWS Console. Go to CloudFront, navigate to the relevant CloudFront distribution, then copy Distribution domain name.

Paste it into a browser and…it kind of works!

But if you look in the Network tab of DevTools you’ll see several 403 errors. It’s not a show-stopper for this simple demo, but it must be fixed for full Nuxt applications.

Deploying static assets to S3

Although we’ve deployed the necessary resources to AWS, there’s still a problem.

Our static resources S3 bucket is empty.

So we need to copy .output/public into the bucket. The easiest way is to use the serverless-s3-sync plugin that syncs a local directory to S3 with a single command.

Install the plugin:

npm i -D serverless-s3-sync

Add this plugins configuration to the top level of serverless.yml.

plugins:
  - serverless-s3-sync

Now add the following custom key, where Serverless Framework looks for plugin configuration.

custom:
  s3Sync:
    buckets:
      - bucketName: ${param:staticResourceBucketName}
        localDir: .output/public
        bucketPrefix: static
        deleteRemoved: true
  • bucketName comes from the parameter defined earlier.
  • localDir set to the directory generated during nuxt build.
  • bucketPrefix sets a static directory into which files are copied.
  • deleteRemoved a value of true deletes files remotely as well as locally.

The plugin will now performs a sync during deployment, so run sls deploy again.

Navigate to the S3 bucket in the AWS console, and look for a static directory.

Refresh the page from earlier, and voila! No errors.

Final thoughts

Deploying Nuxt to Lambda isn’t hard when you know which knobs to turn.

There are two more things to add that will supercharge your Nuxt deployment:

  1. Register a domain name via Route53, create a certificate, and add both to your CloudFront configuration. Then add a DNS record so you can access the application on your own domain name, like a big boy.
  2. Add CORS configuration to your HTTP API to ensure pages served from your domain can access the API.

Ready to deploy Nuxt to AWS Lambda like a pro? Developing Vue applications might never be the same again.

Resources

This serverless.yml deploys a Nuxt application to AWS Lambda. Tweak until satisfied.

service: nuxt-lambda-demo
frameworkVersion: '4'
provider:
  name: aws
  runtime: nodejs18.x
  region: us-east-1
  deploymentMethod: direct
package:
  patterns:
    - '!**'
    - '.output/server/**'
functions:
  nuxtSSREngine:
    handler: .output/server/index.handler
    events:
      - httpApi:
          path: /{proxy+}
          method: get
    environment:
      NUXT_APP_CDN_URL: !Sub "https://${CloudFrontDistribution.DomainName}/static/"
params:
  default:
    staticResourceBucketName: ${self:service}-static-resources
plugins:
  - serverless-s3-sync
custom:
  s3Sync:
    buckets:
      - bucketName: ${param:staticResourceBucketName}
        localDir: .output/public
        bucketPrefix: static
        deleteRemoved: true
resources:
  Resources:
    StaticResourceBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ${param:staticResourceBucketName}
        PublicAccessBlockConfiguration:
          BlockPublicAcls: true
          BlockPublicPolicy: true
          IgnorePublicAcls: true
          RestrictPublicBuckets: true
    StaticResourceOriginAccessControl:
      Type: AWS::CloudFront::OriginAccessControl
      Properties:
        OriginAccessControlConfig:
          Name: ${self:service}-origin-access-control
          OriginAccessControlOriginType: s3
          SigningBehavior: always
          SigningProtocol: sigv4
    StaticResourceBucketPolicy:
      Type: AWS::S3::BucketPolicy
      Properties:
        Bucket: !Ref StaticResourceBucket
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
            Sid: AllowCloudFrontServicePrincipalReadOnly
            Effect: Allow
            Principal:
              Service: cloudfront.amazonaws.com
            Action: s3:GetObject
            Resource: !Sub "arn:aws:s3:::${StaticResourceBucket}/*"
            Condition:
              StringEquals:
                AWS:SourceArn: !Sub "arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}"
    CloudFrontDistribution:
      Type: AWS::CloudFront::Distribution
      Properties:
        DistributionConfig:
          Enabled: true
          DefaultCacheBehavior:
            TargetOriginId: "nuxt-ssr-engine"
            ViewerProtocolPolicy: redirect-to-https
            CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # AWS Managed Cache Policy(CachingDisabled)
          CacheBehaviors:
            - PathPattern: "/static/*"
              TargetOriginId: "nuxt-static-resources"
              ViewerProtocolPolicy: redirect-to-https
              CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # AWS Managed Cache Policy(CachingDisabled)
          Origins:
            - Id: "nuxt-static-resources"
              DomainName: !GetAtt StaticResourceBucket.RegionalDomainName
              OriginAccessControlId: !GetAtt StaticResourceOriginAccessControl.Id
              S3OriginConfig: {}
            - Id: "nuxt-ssr-engine"
              DomainName: !Select [1, !Split ["//", !GetAtt HttpApi.ApiEndpoint]]
              CustomOriginConfig:
                OriginProtocolPolicy: https-only