DEV Community

Cover image for How to host a Sapper.js SSR app on Firebase.
Eckhardt
Eckhardt

Posted on • Edited on

How to host a Sapper.js SSR app on Firebase.

I've been spending about two days scouring the net on trying to find the optimal way to integrate Sapper with Firebase. It's not as easy as it sounds.

Link to example site

What is Sapper?

Sapper is a framework for building extremely high-performance web apps. It is a compiler actually. All your code gets built and optimized ready for production.

It's built on Svelte.js. Sapper has many cool features and I recommend you check the docs out Sapper. The Framework allows for SSR capabilities, but can also generate a static site with npm run export.

What is Firebase

Firebase is Google's platform for "Building apps fast, without managing infrastructure". Basically, in a few commands you can have your static site hosted, hooked up to a database and have authentication readily available.

Other than static sites, Firebase also explores the realm of "serverless" by way of "Functions". These are basically small pieces of logic that you can call either from your app, or when some sort of update in terms of authentication or database occurs.

Functions are useful for:

  • Getting sensitive logic out of the client
  • Performing some action on a piece of data (eg. sanitizing) before it gets inserted into the database.
  • In our case, helping our code serve up a Server Side Rendered application

Benefits of server-side

  • SEO: Crawlers can better index your pre-generated markup.
  • PERFORMANCE: You don't need to serve up bloated assets and you can do some server side caching.

Why the combination of Firebase x Sapper?

Firebase development is really fast and easy, so is Sapper's. I thought: "Why not best of both worlds?". I can have all the niceties of Firebase authentication, database and CDN hosting - along with the speed of development, size of app and general awesomeness of Sapper.

Let's get started

First, we'll need to install the necessary dependencies. (I'm assuming that you already have Node.js version >= 10.15.3 and the accommodating NPM version on your system).

Installing Sapper

Sapper uses a tool called degit, which we can use via npx:

$ npx degit sveltejs/sapper-template#rollup sapper_firebase

$ cd sapper_firebase && npm install # install dependencies

Adding firebase to the project

  • Note: You will need the Firebase CLI tool for this: npm i -g firebase-tools@latest

Before the next step:

You need to go to Firebase and log in to a console. Here you have to create a new Firebase project. When the project is created, on the home page click the </> button to add a new web-app. Name it whatever you like.

Now we can proceed.

# in /sapper_firebase
$ firebase login # make sure to login to your console account

$ firebase init functions hosting

This will return a screen where you can select new project:

Select a default Firebase project for this directory: 
❯ sapper-firebase (sapper-firebase) # select your project

Follow the steps:


? What do you want to use as your public directory? static
? Configure as a single-page app (rewrite all urls to /index.html)? N
? Choose your language: Javascript.
? Do you want to use ESLint to catch probable bugs and enforce style? N
? Do you want to install dependencies with npm now? Y

Then let your functions dependencies install.

Changing our firebase.json

The goal we want to achieve by changing the firebase.json is to:

  1. Serve our project's static files through Firebase hosting.
  2. Redirect each request to the Function that we're going to create (I'll explain this in detail).

Here's the updated firebase.json:

{
  "hosting": {
    "public": "static",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [{
      "source": "**",
      "function": "ssr"
    }]
  }
}

The rewrite is checking for requests under any route, and basically letting the function named 'ssr' handle it.

Edit: I'm afraid there's more.. Recently an error pops up if the firebase-admin dependency in functions/package.json is greater than 7, so just set it to:

"firebase-admin": "^7.0.0"

Remember to:

cd functions && npm install

So let's create the 'ssr' function:

/sapper_firebase/functions/index.js

const functions = require('firebase-functions');

exports.ssr = functions.https.onRequest((_req, res) => res.send('hello from function'));

This is only temporary so we can test our function. We can do so by running:

$ firebase serve

if you get this message:

⚠ Your requested "node" version "8" doesn't match your global version "10"

You can go to functions/package.json and set:

"engines": {
    "node": "10"
}

Other than that, after running firebase serve and going to http://localhost:5000, you should see this:

Testing the function

We're seeing that because Firebase created an index.html file in our static floder. We can safely delete it and we can go to http://localhost:5000/ and should now see:

Hoorah!

Now that we've proven our function to work and redirect requests, we need to mess with Sapper a bit, to get our function to run our server. Let's start off by editing the src/server.js file, our goals are:

  1. We want to export the sapper middleware that is generated on build.
  2. We want to only run the local server on npm run dev.

src/server.js

import sirv from 'sirv';
import polka from 'polka';
import compression from 'compression';
import * as sapper from '@sapper/server';

const { PORT, NODE_ENV } = process.env;
const dev = NODE_ENV === 'development';

if (dev) {
  polka() // You can also use Express
    .use(
    compression({ threshold: 0 }),
    sirv('static', { dev }),
    sapper.middleware()
    )
    .listen(PORT, err => {
    if (err) console.log('error', err);
    });
}

export { sapper };

We can test if npm run dev works by letting it build and visit http://localhost:3000. It should show a picture of Borat (Standard Sapper humor).

But this is still only our Sapper server, we want to run it from our Firebase function. First we need to install express:

$ cd functions
$ npm install express sirv compression polka

Note: 'sirv' 'compression' and 'polka' are required, because of the built directory that depends on it. We'll only use express in our function though. But if we exclude the others, our function will fail on deploy.

After installing express and the other dependencies, we first want to tweak our workflow to build a copy of the project into functions. We can do this by editing the npm scripts:

Windows users: please use the guide at the end of the post for your scripts

...
"scripts": {
    "dev": "sapper dev",
    "build": "sapper build --legacy && cp -R ./__sapper__/build ./functions/__sapper__",
    "prebuild": "rm -rf functions/__sapper__/build && mkdir -p functions/__sapper__/build",
    "export": "sapper export --legacy",
    "start": "npm run build && firebase serve",
    "predeploy": "npm run build",
    "deploy": "firebase deploy",
    "cy:run": "cypress run",
    "cy:open": "cypress open",
    "test": "run-p --race dev cy:run"
},
...

This will copy all the necesarry files to the functions folder before hosting or serving them locally.

Try it with:

$ npm start # now using firebase

You should see the message from earlier!

Serving our Sapper app via Functions and express

We can plug an express app into our function, use our imported Sapper middleware on express and serve our SSR app seemlessly. The static folder is also being served via the very fast Firebase CDN.

functions/index.js

const functions = require('firebase-functions');
const express = require('express');

// We have to import the built version of the server middleware.
const { sapper } = require('./__sapper__/build/server/server');

const app = express().use(sapper.middleware());

exports.ssr = functions.https.onRequest(app);

Hosting your Sapper Firebase SSR app locally

All you have to do now is:

$ npm start

AAANND...

HOORAAAH

If you see this image on http://localhost:5000 your app is being served by a local firebase functions emulator!

To confirm that it is SSR, just reload a page and check the page source, all the markup should be prerenderd on initial load. Also check out your terminal, you should see all kinds of requests as you navigate your app!

Hosting your app

Because of our neat NPM scripts hosting/deploying is as easy as:

$ npm run deploy

This will take a while, to get your files and functions up. But here's my version online SSRing like a boss.

Windows users

You will need to do some extra stuff to get this working...

  1. Add some npm packages to help with removing and copying stuff:

npm install -D rimraf mkdirp ncp

The -D flag adds it to dev dependencies, since that's where we need it. Change your scripts to this:

...
"build": "sapper build --legacy && ncp ./__sapper__/build ./functions/__sapper__/build",
"prebuild": "rimraf - functions/__sapper__/build && mkdirp functions/__sapper__/build",
...

These solutions are hacky and this post might bite me in the future due to rapid changes in sapper and firebase stuff. Thank you to Diogio Marques for requesting this in comments.

Thank you

This is one of my first posts that I actually feel could help someone out, since it took me hours to figure out (Maybe I'm just slow). But I typed it out since I can't make videos on my Youtube Channel, because of time issues.

But if you did enjoy this, or have questions, chat to me on twitter @eckhardtdreyer. Until next time.

Top comments (34)

Collapse
 
mortscode profile image
Mort

Amazing work. For some reason, I'm not getting any images or styling when I run npm start. Everything renders correctly with npm run dev, but no luck on the firebase side.

Any thoughts?

Collapse
 
charukahs profile image
Charuka Samarakoon

Not a solution but a head relief:
Images and styling will work after running npm deploy and visit the hosted url

Don't know the exact reason, but probably due to firebase hosting not mounting locally at /

Collapse
 
mortscode profile image
Mort

In my Firebase Console, if I click on the link under "Hosting" the site seems to be loading just fine. For some reason, I'm not getting anything but html on my localhost:5000 when I run npm start.

Collapse
 
eckhardtd profile image
Eckhardt

Mm, difficult to say, do you have a repo maybe? Is your firebase.json set like in the article? And are your external css and image files in your static folder?

Thread Thread
 
mortscode profile image
Mort • Edited

repo: github.com/mortscode/sapper-firebase

The issue in my browser console is that all of the asset urls are like so: http://localhost:5000/[firebase-project-name]/us-central1/ssr/global.css. They're all returning 404's.

Is there a location in the app where I should be defining that path?

One more thing, the command firebase init functions hosting didn't give me the hosting options in the console. I had to run firebase init hosting separately.

Thread Thread
 
focusat profile image
focus-at • Edited

works for me

const functions = require("firebase-functions");

// We have to import the built version of the server middleware.
const { sapper } = require("./sapper/build/server/server");
const middleware = sapper.middleware();

exports.ssr = functions.https.onRequest((req, res) => {
req.baseUrl = '';
middleware(req, res);
});

Thread Thread
 
eckhardtd profile image
Eckhardt

Hi Mort,

So sorry for the late reply. Have been taking a screen break.

Have you figured it out? I will look at your repo in my breaks.

Regards

Thread Thread
 
eckhardtd profile image
Eckhardt

I can't seem to replicate your problem, my urls are correct out of the box. I'll have to spend time finding the issue. I did however notice something else regarding versions of dependencies in the ./functions folder. It seems that I have to use the exact versions in package.json in functions here github.com/Eckhardt-D/sapper-fireb.... Strange. I'll explore. Sorry I don't have an off-the-cuff response.

Thread Thread
 
aslak01 profile image
aslak01

I'm having the exact same issue as Mort. I'm using newest versions in the functions package.json, as I got an error trying to use firebase admin 7.0.0. Once built and uploaded it works fine, but on localhost:5000 the firebase project name and useast1 time zone stuff shows up in the urls and causes 404s.

Thread Thread
 
mikedane profile image
Mike Dane

focus-at's solution worked for me, I'm using latest firebase-admin. I suspect this is a problem with the baseUrl var

Thread Thread
 
olyno profile image
Olyno

Hi,
Same issue here with latest versions. Any fix?

Thread Thread
 
bodhiz profile image
bodhiz

focus-at workaround worked for me as well.

Collapse
 
nedwards profile image
n-edwards

Thanks very much for breaking this down.

I also tried following this video,
youtu.be/fxfFMn4VMpQ
and found the workflow a bit more manageable when creating a Firebase project first, and then adding in a new Sapper project. I got through it without any issues, on Windows. Would have much preferred it to be a write-up like yours though, so here's a summary:

Create a new empty folder, then navigate to it in the VS Code terminal.

firebase init

  • functions
  • hosting
  • use an existing project you've created on Firebase
  • no ESLint
  • don't install dependencies now
  • public directory: functions/static (Sapper project will go into functions folder)
  • SPA: no

Move or rename package.json, and delete .gitignore (sapper will create these for us later)

cd functions

npx degit sveltejs/sapper-template#rollup --force

Copy contents of scripts block (Firebase commands) from old package.json into scripts block of new package.json that was generated by Sapper.
Rename Firebase's start command to fb_start.

Copy entire engines block from old to new package.json, and change node version to 10.

Copy over contents of dependencies and devDependencies blocks.

Delete old package.json, once all Firebase stuff is moved over, and save the new Sapper one.

Remove polka from dependencies in package.json.

npm install --save express
npm install

server.js:

  • import express instead of polka
  • change function to: const expressServer = express()...
  • change .listen to if (dev) { expressServer.listen ... }
  • export { expressServer }

index.js:

  • const {expressServer} = require('./__sapper__/build/server/server')
  • exports.ssr = functions.https.onRequest(expressServer);

npm run build
npm run dev

localhost:3000 will show the Firebase default index.html from static folder, which can be deleted.
Page reload will bring up Sapper project.

firebase.json:

  • "rewrites": [ { "source": "**", "function": "ssr" }]

npm run build
firebase deploy

Visit app and click around, refresh to verify functionality.

Try Postman, send a GET to your project URL.
In output, look for confirmation that content is SSR.

package.json:

  • "deploy": "npm run build && firebase deploy"

Nav.svelte:

  • add a new li to the navbar, for a new page

routes:

  • create new .svelte page, and add some quick HTML content

npm run deploy

Verify new content shows.
Run audit from Chrome dev tools.

Collapse
 
xanderjakeq profile image
Xander Jake de los Santos

trying to follow this tutorial. But when I try to run the final setup of firebase functions with sapper, I get this error. Any idea why?

Error: ENOENT: no such file or directory, open '__sapper__/build/build.json'

Collapse
 
eckhardtd profile image
Eckhardt

I have not had this error. Keep in mind this is an old post and perhaps things are outdated. I don't keep this post updated, but I'll consider doing an edit to address all the questions here. Have you checked out the repo at github.com/Eckhardt-D/sapper-fireb... ? Perhaps you can find something there.

Collapse
 
xanderjakeq profile image
Xander Jake de los Santos

I tried running the repo but I'm still getting the same error. Maybe I did something wrong with my setup. I'll find a more recent tutorial for now.

Thanks for your time!

Thread Thread
 
eckhardtd profile image
Eckhardt

I'll have a thorough look at the repo this weekend and update the post accordingly. Sorry I can't give you a fast answer. Hope you get it sorted, if not maybe check in here the weekend or watch the repo for changes :).

Collapse
 
kahilkubilay profile image
Kubilay Kahil • Edited

You have to get build before npm start command. You should npm run build in the "functions" folder.

Collapse
 
jofont profile image
Diogo Fontainhas Garcia Marques

Trying to get this to work but I'm getting stuck in the editing the npm scripts.
First, i'm assuming we need to edit the npm scripts from sapper (so not the package.json from inside functions), secondly, when I edit the scripts, the prebuid doesn't run, there are two errors; the first says that rm is not a valid command (fair enough, changed to del) then it didn't like the mkdir -p so I removed the -p. At the end it still doesn't run and says Invalid switch - "__sapper__"., Any idea why? Thank you for the article anyway!

Collapse
 
eckhardtd profile image
Eckhardt

Hi!

It would really be easier for me if I can get a link to a github repo and explore myself so I can give a complete answer.

I’d like to update this post if there were any changes to the workflow because of updates etc.

Also what OS are you running? I haven’t tested this on all platforms. You would be a huge help here!

Thanks.

Collapse
 
jofont profile image
Diogo Fontainhas Garcia Marques

Hello!
Sorry for the delay. So here you go: gitlab.com/JoFont/sapper-firebase-...

The repo has a Readme with the same info I wrote in the first comment along with some images.

I'm running Windows 10. Thank you very much for you help, and have a great day!

Thread Thread
 
eckhardtd profile image
Eckhardt • Edited

Hi! No problem.

I just cloned your repo and holy ... is Windows copy and folder deletion a pain... Here's my solution (Also adding to post):

  1. Add some npm packages to help with removing and copying stuff:

npm install -D rimraf mkdirp ncp

The -D flag adds it to dev dependencies, since that's where we need it. Change your scripts to this:

...
"build": "sapper build --legacy && ncp ./__sapper__/build ./functions/__sapper__/build",
"prebuild": "rimraf - functions/__sapper__/build && mkdirp functions/__sapper__/build",
...

I'm afraid there's more.. An error pops up if the firebase-admin dependency in functions/package.json is greater than 7, so just set it to:

"firebase-admin": "^7.0.0"

Remember to:

cd functions && npm install

These solutions are hacky and this post might bite me in the future due to rapid changes in sapper and firebase stuff. But this got it working for me on Windows 10 using your repository. Regards!

Thread Thread
 
jofont profile image
Diogo Fontainhas Garcia Marques • Edited

Ok! So we do have success with your changes!

Some notes:

  1. Couldn't replicate error with firebase-admin version. Working fine with version 8.2.0, actually if I were to set it to 7.0.0, firebase throws an error.

  2. npm start does work but as someone mentioned earlier, I also do not get styles and assets. No issues I think, because you don't really need npm start, just use npm run dev for development and npm run deploy when deploying to Firebase.

  3. Finally, this does indeed look like a very half-arsed way to make Firebase deliver Sapper SSR, because I feel like it's one update from Firebase or one dependency away from breaking (at least on windows). So perhaps either thinking on a more stable way of implementing this or waiting for firebase for a proper Node.js server would be the way. Either way at least for now it works, though I'll probably stick with AWS for production runs.

Thank you very much @eckhardtd for the tutorial! Have a great one!

Collapse
 
yiddishekop profile image
Yehuda Neufeld

Brilliant article!!!
I'm really thinking a lot about SSR vs static-site [sapper export].
With Firebase we can host the static files of the site & still use user authentication for login etc.
Which makes me wonder why shouldn't I just serve a static site [which is MUCH faster than SSR]?!

Collapse
 
eckhardtd profile image
Eckhardt

Hey, thank you! And yes. There is a big movement towards static or even β€˜JAMstack’, personally I’d use static as much as possible, since it almost always fulfills the needs of my clients. But sometimes I need more complicated server architecture and prefer not having to manage client and server explicitly. With Sapper or Nuxt I can manage both in one project. All up to preference though.

Collapse
 
vorcigernix profile image
Adam Sobotka

Oh. Warning:
"Firebase projects on the Spark plan can make only outbound requests to Google APIs. Requests to third-party APIs fail with an error. For more information about upgrading your project, see Pricing."
I followed all the guide just to find that I can't use it. Nice writing anyway.

Collapse
 
bastin profile image
Sebastian Neumair

Thanks a lot for the step-by-step guide.
I created a small rss reader with svelte/sapper, in which I have implemented a backend-api to get and delete a list representing the rss feed. How do I get the backend to work as well? Currently I get 404s.

For the purposes of handling all requests to the /api, i created an api.js:

import express from 'express'
import Parser from 'rss-parser'

const rssList = new Set()

const parser = new Parser()

export default (server) => {
    const router = express.Router()

    router.post('/add', (req, res) => {
        const { url } = req.body

        if (rssList.has(url)) {
            return res.send({ added: false, rssList: [...rssList] })
        }

        rssList.add(url)
        return res.send({ added: true, rssList: [...rssList] })
    })

    router.post('/delete', (req, res) => {
        const { url } = req.body

        if (rssList.has(url)) {
            rssList.delete(url)
            return res.send({ deleted: true, rssList: [...rssList] })
        }

        return res.send({ deleted: false, rssList: [...rssList] })
    })

    router.get('/list', (_, res) => {
        res.send({ rssList: [...rssList] })
    })

    router.get('/refresh', async (req, res, next) => {
        try {
            let result = []
            for (const url of rssList) {
                const feed = await parser.parseURL(url)
                result.push(...feed.items)
            }

            result = result.sort((a, b) => (a.isoDate > b.isoDate ? 1 : 0))

            res.send({ result })
        } catch (err) {
            console.log(err)
            next(err)
        }
    })

    return router
}

In my server.js I import the apiRouter form api.js and simply added it as middleware:

    server.use('/api', apiRouter())

You can find the repo (branch) here: github.com/basti-n/rss-reader-sapp...

Collapse
 
mikenikles profile image
Mike

An alternative approach is to use Firebase Hosting for static assets and Cloud Run for the server-side part, instead of Cloud Functions. I recently wrote about that in detail, including a template repo.

I'd love to hear feedback if anyone gets a chance to review the article.

mikenikles.com/blog/firebase-hosti...

Collapse
 
bodhiz profile image
bodhiz

Has someone already setup an API proxy with this Firebase/Sapper configuration ?
Would be useful to call the local instance of functions when in dev mode.

Collapse
 
bketelsen profile image
Brian Ketelsen

brilliant! Thanks for writing it up