That's a mouthful of a title right there. Don't let it scare you away. What it boils down to is rather simple.
We want the best of both worlds. The SEO boost server-side rendering provides, and the speed of a Single Page Application. All this while hosted basically for free in a serverless environment on AWS Lambda.
TL;DR
Here's a quick overview of what we'll be building for you to get up to speed. Feel free to jump to the step that interests you the most. Don't mind me guilt tripping you into reading the whole thing... * stares guilt trippingly * 😐
- What're we building?
- Configure and install dependencies
- Build the app with the Serverless Framework and Nuxt.js
- Deploy the app to AWS Lambda
- Test the app with Tracetest
Note: The code we will write is already on GitHub if you need further reference or miss any steps, feel free to check it out.
What're we building?
Well, first thing's first. We want a super fast Single Page Application. But, this usually comes with a cost. Lousy SEO capabilities. That won't do, meaning we also want the app to have server-side rendering. Okay, sounds simple. We'll grab Nuxt.js, which is a framework for creating universal Vue.js applications, and configure it to server-side render our pages.
To accomplish this we need to spin up a simple Express server and configure the Nuxt renderer to serve files through Express. It is way simpler than it sounds.
However, the key takeaway here is the word server. Ew, we don't like mentioning that word. So, what do we need to do? Well, deploy this whole application to AWS Lambda! It is a tiny Node.js instance after all.
But this raises a concern. How to monitor and debug it if everything goes horribly wrong? I usually have Tracetest opened in a separate tab to test all my serverless resources in real-time.
Phew, with that out of the way, let's get crackin'!
Configure and install dependencies
As always, we're starting with the boring part, setting up the project and installing dependencies.
1. Install the Serverless Framework
In order for serverless development to not be absolute torture, go ahead and install the Serverless framework.
$ npm i -g serverless
Note: If you’re using Linux or Mac, you may need to run the command as sudo
.
Once installed globally on your machine, the commands will be available to you from wherever in the terminal. But for it to communicate with your AWS account you need to configure an IAM User. Jump over here for the explanation, then come back and run the command below, with the provided keys.
$ serverless config credentials \
--provider aws \
--key xxxxxxxxxxxxxx \
--secret xxxxxxxxxxxxxx
Now your Serverless installation knows what account to connect to when you run any terminal command. Let’s jump in and see it in action.
2. Create a service
Create a new directory to house your Serverless application services. Fire up a terminal in there. Now you’re ready to create a new service.
What’s a service you ask? View it like a project. But not really. It’s where you define AWS Lambda functions, the events that trigger them and any AWS infrastructure resources they require, all in a file called serverless.yml.
Back in your terminal type:
$ serverless create --template aws-nodejs --path serverless-side-rendering-vue-nuxt
The create command will create a new service. Shocker! But here’s the fun part. We need to pick a runtime for the function. This is called the template. Passing in aws-nodejs
will set the runtime to Node.js. Just what we want. The path will create a folder for the service.
3. Install npm modules
Change into the serverless-side-rendering-vue-nuxt folder in your terminal. There should be three files in there, but for now, let's first initialize npm.
$ npm init -y
After the package.json
file is created, you can install a few dependencies.
$ npm i axios nuxt express serverless-http serverless-apigw-binary
These are our production dependencies, and I'll go into more detail explaining what they do a bit further down. Apart from them, we need one more as a development dependency. This one will let us tie a domain to our endpoints. Sweet!
$ npm i --save-dev serverless-domain-manager
Now, your package.json
should look something like this.
{
"name": "serverless-side-rendering-vue-nuxt",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": { // <= add these scripts
"dev": "nuxt",
"deploy": "nuxt build && sls deploy"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"axios": "^0.18.0",
"express": "^4.16.3",
"nuxt": "^1.4.2",
"serverless-apigw-binary": "^0.4.4",
"serverless-http": "^1.6.0"
},
"devDependencies": {
"serverless-domain-manager": "^2.6.0"
}
}
We also need to add two scripts, one for running nuxt
on our local dev machine and one for building and deploying the app. You can see them in the scripts
section of the package.json
.
4. Configure the serverless.yml file
Moving on, let's finally open up the project in a code editor. Check out the serverless.yml file, it contains all the configuration settings for this service. Here you specify both general configuration settings and per function settings. Your serverless.yml will be full of boilerplate code and comments. Feel free to delete it all and paste this in.
# serverless.yml
service: serverless-side-rendering-vue-nuxt
provider:
name: aws
runtime: nodejs8.10
stage: ${self:custom.secrets.NODE_ENV}
region: eu-central-1
environment:
NODE_ENV: ${self:custom.secrets.NODE_ENV}
functions:
nuxt:
handler: index.nuxt
events:
- http: ANY /
- http: ANY /{proxy+}
plugins:
- serverless-apigw-binary
- serverless-domain-manager
custom:
secrets: ${file(secrets.json)}
apigwBinary:
types:
- '*/*'
customDomain:
domainName: ${self:custom.secrets.DOMAIN}
basePath: ''
stage: ${self:custom.secrets.NODE_ENV}
createRoute53Record: true
The functions
property lists all the functions in the service. We will only need one function because it will run the Nuxt rendering. It works by spinning up a tiny Express app, connecting the Nuxt renderer middleware to the Express router and passing the app to the serverless-http module. In turn, this will bundle the whole Express app into a single lambda function and tie it to an API Gateway endpoint. Under the functions property, you can see a nuxt function that will have a handler named nuxt
in the index.js
file. API Gateway will proxy any and every request to the internal Express router which will tell the Nuxt renderer to render our Vue.js pages. Woah, that sounds complicated! But it's really not. Once we start writing the code you'll see how simple it really is.
We've also added two plugins, the serverless-apigw-binary
for letting more mime types pass through API Gateway and the serverless-domain-manager
which lets us hook up domain names to our endpoints effortlessly.
We also have a custom
section at the bottom. The secrets
property acts as a way to safely load environment variables into our service. They're later referenced by using ${self:custom.secrets.<environment_var>}
where the actual values are kept in a simple file called secrets.json
.
Apart from that, we're also letting the API Gateway binary plugin know we want to let all types through, and setting a custom domain for our endpoint.
That's it for the configuration, let's add the secrets.json
file.
5. Add the secrets file
We all know pushing private keys to GitHub kills baby penguins. Let's not do that. Handling this with the Serverless Framework is simple. Add a secrets.json
file and paste this in.
{
"NODE_ENV": "dev",
"DOMAIN": "vue-ssr.your-domain.com"
}
Now, only by changing these values you can deploy different environments to different stages and domains. Pretty cool.
Build the app with the Serverless Framework and Nuxt.js
We'll use Nuxt.js to build our universal Vue.js app. What does this mean? Well, let's keep it simple, it's just a server-side rendered single page application. Meaning you don't need to worry about SEO because it'll render the JavaScript before sending it to the client. But, once it's loaded in on the client side, it won't ask for the file again, and cache it instead. More speed! I love it.
Let's jump in.
1. Setting up the Nuxt.js server(less)-side rendering
For Nuxt to work at all we need a nuxt.config.js
file to add our build configuration.
// nuxt.config.js
module.exports = {
mode: 'universal',
head: {
title: 'Vue Nuxt Test',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: 'Nuxt.js project' }
]
},
build: {
vendor: ['axios'],
publicPath: `/${require('./secrets.json').NODE_ENV}/_nuxt/` // <= add the path to the cached files
},
srcDir: 'client/',
performance: {
gzip: false
},
router: {
base: `/`
},
dev: false
}
You can see we required the secrets.js file in order to load our stage in the publicPath
URL to the static files. You'll see why this is important once we add the route in the Express router below. Also, check the srcDir
, it specifies the name of the folder where our client-side files are located.
Once this is added, create another file named nuxt.js. Really intuitive, I know.
// nuxt.js
const express = require('express')
const app = express()
const { Nuxt } = require('nuxt')
const path = require('path')
app.use('/_nuxt', express.static(path.join(__dirname, '.nuxt', 'dist')))
const config = require('./nuxt.config.js')
const nuxt = new Nuxt(config)
app.use(nuxt.render)
module.exports = app
This is pretty straightforward. We're grabbing Express and Nuxt, creating a static route with express.static
and passing it the directory of the bundled JavaScript that Nuxt will create. Here the path is /_nuxt
but because API Gateway adds the stage as a postfix, we needed to specify it in the publicPath
in the above mentioned nuxt.config.js
file.
Once the configuration is loaded, there's nothing left to do except pass the nuxt.render
middleware to the Express app.
Now, the app needs to be hooked up to serverless-http
and exported as a lambda function. Create an index.js
file and paste this in.
// index.js
const sls = require('serverless-http')
const binaryMimeTypes = require('./binaryMimeTypes')
const nuxt = require('./nuxt')
module.exports.nuxt = sls(nuxt, {
binary: binaryMimeTypes
})
As you can see we also need to create binaryMimeTypes.js
file to hold all the mime types we want to enable. It'll just a simple array which we pass into the serverless-http
module.
// binaryMimeTypes.js
module.exports = [
'application/javascript',
'application/json',
'application/octet-stream',
'application/xml',
'font/eot',
'font/opentype',
'font/otf',
'image/jpeg',
'image/png',
'image/svg+xml',
'text/comma-separated-values',
'text/css',
'text/html',
'text/javascript',
'text/plain',
'text/text',
'text/xml'
]
Sweet, that's it regarding the Nuxt setup. Let's jump into the client-side code!
2. Writing client-side Vue.js
In the root of your project create a new folder and name it client
. If you scroll up we set the srcDir
in the nuxt.config.js
file to point to a directory named client
.
In this client
folder, create three more folders named, components
, layouts
, pages
. Once insidelayouts
folder, create a new file with the name default.vue
, and paste this in.
// client/layouts/default.vue
<template>
<div>
<navbar/>
<nuxt/>
</div>
</template>
<script>
import navbar from "~/components/navbar";
export default {
components: { navbar }
};
</script>
The default view will have the <navbar/>
component and the <nuxt/>
component with rendered content from Nuxt.
Now add the navbar.vue
file in the components
folder.
// client/components/navbar.vue
<template>
<nav class="nav">
<ul>
<li>
<nuxt-link to="/">Home</nuxt-link>
</li>
<li>
<nuxt-link to="/dogs">Dogs</nuxt-link>
</li>
<li>
<nuxt-link to="/dogs/shepherd">Only Shepherds</nuxt-link>
</li>
</ul>
</nav>
</template>
This is an incredibly simple navigation that'll be used to navigate between some cute dogs. It'll make sense once we add something to the pages
folder.
In the pages
folder create an index.vue
file and add the code below.
// client/pages/index.vue
<template>
<div>
<h1>This is the Front Page.</h1>
<h3>Random dog of the day:</h3>
<img :src="dog.url" alt="">
</div>
</template>
<script>
import axios from "axios";
export default {
async asyncData({ params }) {
const { data } = await axios.get(
"https://api.thedogapi.com/v1/images/search?limit=1"
);
return { dog: data[0] };
}
};
</script>
The index.vue
file will be rendered on the root path of our app. It calls a dog API and will show a picture of a cute dog. To create more routes, create a sub-folder called dogs
and create an index.vue
file and a _breed.vue
file in there. The index.vue
will be rendered at the /dogs
route while the _breed.vue
will be rendered at /dogs/:breed
where the :breed
represents a route parameter.
Add this to the index.vue
in the dogs
directory.
// client/pages/dogs/index.vue
<template>
<div>
<h1>Here you have all dogs.</h1>
<ul>
<li v-for="dog in dogs" v-bind:key="dog.id">
<img :src="dog.url" alt="">
</li>
</ul>
</div>
</template>
<script>
import axios from "axios";
export default {
async asyncData({ params }) {
const { data } = await axios.get(
"https://api.thedogapi.com/v1/images/search?size=thumb&limit=10"
);
return { dogs: data };
},
head() {
return {
title: "Show all dogs!",
meta: [
{
hid: "description",
name: "description",
content: `Hello Dogs 👋`
}
]
};
}
};
</script>
And, another snippet in the _breed.vue
file in the dogs
folder.
// client/pages/dogs/_breed.vue
<template>
<div>
<h2>Dog breed: {{ breed }}</h2>
<ul>
<li v-for="dog in dogs" v-bind:key="dog.id">
<img :src="dog.url" alt="">
</li>
</ul>
</div>
</template>
<script>
import axios from "axios";
export default {
async asyncData({ store, route }) {
const { data } = await axios.get(
"https://api.thedogapi.com/v1/images/search?size=thumb&has_breeds=true&limit=50"
);
const reg = new RegExp(route.params.breed, "g");
const filteredDogs = data.filter(dog =>
dog.breeds[0]
.name
.toLowerCase()
.match(reg)
);
return { dogs: filteredDogs, breed: route.params.breed };
},
head() {
return {
title: `${this.breed} Dog`,
meta: [
{
hid: "description",
name: "description",
content: `You are ${this.breed} hello 👋`
}
]
};
}
};
</script>
As you can see in these files there's a head()
function. It will add custom fields in the <head>
of your page, giving it proper SEO support!
Note: If you're stuck, here's what the code looks like in the repo.
Let's deploy it and see if it works.
Deploy the app to AWS Lambda
At the very beginning, we added a script to our package.json
called deploy
. It'll build the Nuxt app and deploy the serverless service as we specified in the serverless.yml
.
All you need to do is run:
$ npm run deploy
You'll see the terminal return some output with the endpoint for your app. But, there's one more thing for us to do. We need to add the domain. We've already added the configuration in the serverless.yml
but there's one more command we need to run.
$ sls create_domain
This will create a CloudFront distribution and hook it up to your domain. Make sure that you've added the certificates to your AWS account. It usually takes around 20 minutes for AWS to provision a new distribution. Go have a coffee.
Back? Okay, go ahead and deploy it all once again.
$ npm run deploy
You'll still be able to use the default endpoints, but now you also have it tied up to your domain. Here's what it should look like.
Sweet, your app is up-and-running. Go ahead and try it out.
Test the app with Tracetest
The #1 problem with all serverless applications are their distributed nature. Plain and simple, it's impossibly hard to have an overview of all the things going on. Not to mention how hard it is to debug when something goes wrong.
To calm my fears I use Tracetest. It gives you end-to-end testing & debugging powered by OpenTelemetry.
Thankfully, there's detailed documentation, which makes the onboarding process a breeze. Go ahead and follow the Quick Start guide. Don't forget to come back here though. 😄
That’s in, the app is done. You've created a server-side rendered Vue.js app with Nuxt.js, hosted it in a serverless environment on AWS Lambda, and added a way to monitor and debug your app before your users start complaining. Doesn't get any better than that.
Wrapping up
This is a whole new way of thinking about creating fast and responsive websites. There are absolutely no servers you need to worry about. Just deploy the code and rest assured it'll work. If something breaks, you have Tracetest watching your back!
If you got stuck anywhere take a look at the GitHub repo for further reference, and feel free to give it a star if you want more people to see it on GitHub.
adnanrahic / serverless-side-rendering-vue-nuxt
Sample project for using Nuxt.js to create a server-side rendered Vue.js app on AWS Lambda and AWS API Gateway.
Serverless-side rendering with Vue.js and Nuxt.js
Sample project for using Nuxt.js to create a server-side rendered Vue.js app on AWS Lambda and AWS API Gateway. Can easily integrate with your own API or 3rd party APIs such as headless CMS, e-commerce or serverless architecture.
Full tutorial is here.
If you want to read some of my previous serverless musings head over to my profile or join my newsletter!
Or, take a look at a few of my articles right away:
- Building a serverless contact form with AWS Lambda and AWS SES
- A crash course on Serverless APIs with Express and MongoDB
- Solving invisible scaling issues with Serverless and MongoDB
- How to deploy a Node.js application to AWS Lambda using Serverless
- Getting started with AWS Lambda and Node.js
- A crash course on securing Serverless APIs with JSON web tokens
- Migrating your Node.js REST API to Serverless
- Building a Serverless REST API with Node.js and MongoDB
- A crash course on Serverless with Node.js
I also highly recommend checking out this article about Nuxt.js, and this tutorial about the serverless domain manager.
Hope you guys and girls enjoyed reading this as much as I enjoyed writing it. If you liked it, slap that tiny heart so more people here on dev.to will see this tutorial. Until next time, be curious and have fun.
Disclaimer: Tracetest is sponsoring this blogpost. Use observability to reduce time and effort in test creation + troubleshooting by 80%.
Top comments (24)
Any way I can test my site locally? I've tried using the serverless-offline plugin, but although the local web server is starting up, I get no functional endpoints. Navigating to localhost:3000/ just gives a 404 in the server log.
I was having the same problem, but got it working with the following:
I have no idea why this works, I stole it from the jeehyukwon/nuxt-edge-serverless-template repository.
I believe it's safer if you wait till the renderer is ready. To do so, simply listen to the
render:done
hook. For instance something like should work:setTimeout
doesn't seem very reliable to me :) On my mac I had to wait around 60ms for instance.The following works as well.
With the
nuxt.hook("render:done" () => {})
I was getting random 404 errors when deployed.nuxt.ready()
did the trick for me!Awesome of you to share this! Feel free to add a PR to the GitHub repo with this edit. 👌
Can confirm this solution works. Install serverless-offline with
npm i serverless-offline
. Then in nuxt.js, just replaceapp.use(nuxt.render)
with Pascal's code, above. Run the plugin withsls offline
. Be sure to include thehttp
inhttp://localhost:3000
.Works great, but resulting package is 40MB+
Would be good to see another tutorial on trimming the unused packages. I tried with Exclude/Include in serverless CLI and also the serverless-plugin-optimize and serverless-plugin-include-dependencies, they all had an effect on the output size, but none worked in AWS :(
Shouldn't we compile the application and send only the dist files instead of the entire node_modules folder?
check my answer here: dev.to/ludofleury/comment/icn2
Fantastic post, Adnan. Really enjoying this series of threads relating to serverless.
One (silly?) question.
Is further setup required to be able to serve static assets like images etc.?
Hey Luca
Not a silly question at all! To be direct, no further setup is required. Even though a lambda function is ephemeral, you can store static assets and serve them just as from any other server (check this out in the repo).
However, I would argue against this for a real-world use case. You should really use an S3 bucket to store the images and serve them from there.
Another cool fact is that you can use Nuxt to generate static files and host everything on S3, without using AWS Lambda at all. It all comes down to preference.
Hope this clears some things up. I'm glad you liked the article. Stay in touch or send me a message through the chat if you have any questions. 😃
Very nice example and article! Any thoughts on how to store the static dist files on S3? I mean lambda for the SSR first hit from the client. Then any other nuxt dist files needed once the the SPA is hydrated on the client are then retrieved form the s3 bucket/cloud-front distribution?
Did you found a solution for this?
Alright, I spent my weekend on this, here's the insights:
You need a "manually setup" cloudfront distribution and stop relying on the automated edge optimized domain name by serverless.
You configure the cloudfront distribution with 2 behaviors:
"/" to your API Gateway
"/_nuxt/*" to your s3 bucket.
And you configure manually the custom domain name in cloudfront and in Route53 DNS
Its a bit of initial work but you'll do this only once. You can remove the serverless plugin for domain management.
I used serverless-finch plugin which handle the upload to s3 + a manually s3 bucket setup
You can remove the "app.use('/nuxt', express.static(path.join(_dirname, '.nuxt', 'dist')))" as your lambda won't be called anymore for serving static files.
You need to optimize (aka reduce) your dependencies, rely on nuxt-start (check npm), which is a production-only distribution of nuxt.
Exclusively for aws lamba (not sure for other cloud providers):
So I excluded the directory "node_modules/**" from my serverless deployment. Which means every time I install another npm package, I have to update my layer, you can automate this with a specific ".sh", but it's still cumbersome tbh.
For my serverless deployment, it becomes extremly lightweight and simple:
exclude everything, include only:
The client ones are hosted on s3 thanks to the serverless-finch plugin:
bucketName: ********
distributionFolder: .nuxt/dist/client
keyPrefix: _nuxt
The result is efficient: the lambda size is around 15.7 Kb deployed.
Layers uploaded only once as my deps are not really moving:
I might be writing a blog post about it here on dev.to soon.
I really need your post =)
i can't integrate static with cloudfront
Thanks Ludovic,
I got things running however couldn't find the distribution listed in cloudfront. Hence, unable to use it to proxy to other aws resources... which seems to be a limit in the serverless domain plugin.
Render static images in nuxt when deployed
I cloned the repo and deployed and everything worked until i added local file to the static folder of my nuxt app with other plugins like PWA and router etc. When i reference the images, they don't load and the sw.js script returns a 403 http error.
Please what am i to do to fix this error. Do i have to update the serverless.yml file or what?
Hey there, Nice Post. I made an npm package for handling AWS Lambda responses. Do checkout 😎
AWS-TS This package lets you handle and send responses from AWS lambda with ease. You have the ability to send various types of responses such as JSON or Plain Text without worrying about headers and status codes. You can also enable or disable cors for all or specific responses or set custom headers.
🚀 npmjs.com/package/aws-ts
Awesome article. I stumble upon one thing. My endpoint (nearby region) takes 1 second time to first byte, I am wondering if there is something in configuration that I missed in order to serve it more efficiently.
"This will create a CloudFront distribution and hook it up to your domain. Make sure that you've added the certificates to your AWS account."
I new I will stuck in some place.
As I am totaly new to the AWS... how am I supposed to add the certificates to my AWS account?
Should I change the default "vue-ssr.your-domain.com" and if so to what?
I have no domain on AWS this is my first touch with it.
I ran into the same problem. There are quite a few steps you need to do manually (i.e in the Web Console) before you can use this guide. I won't get into details, but first you need to register a domain name in Route53 (or add a hosted zone with a domain from another registrar, but that is probably more complicated). Then you will need to add a certificate. Easiest way is to use the Certificate Manager. Enter yourdomain.com and *.yourdomain.com in your certificate request, then follow the guide to verify it. After you've done this, you are ready to go with sls create_domain. It will add the vue-ssr subdomain for you.
I tried running the latest in the repo and im getting the following error:
Renderer resources are not loaded! Please check possible console errors and ensure dist..
I believe it run the npm run build on start-sls..anyone having the same issue?
Thank you for a good article. I have a question. Is it possible to use nuxt v2 on this way?
Hi, I was wondering about your thoughts of code compiled to WASM to execute serverlessly.