DEV Community

Cover image for Integrating the WebContainer API with Node.js
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Integrating the WebContainer API with Node.js

Written by Antonello Zanini✏️

Node.js is a popular and powerful platform for building scalable and efficient web applications. However, in some cases, it may be necessary to build a Node.js application for a quick proof-of-concept or in collaboration with other developers.

Creating local environments can be a major inconvenience, particularly when attempting to prototype ideas quickly, experiment with open-source libraries, collaborate with colleagues, or reproduce bugs. This is where the browser-based WebContainers come into play.

This article will discuss how to create full-stack Node.js environments that can be launched within seconds and are immediately accessible and shareable via a single click. We’ll cover:

What are WebContainers?

Browser-based WebContainers, also known as web-based containers, are runtime environments that allow developers to execute code and run applications within a web browser without the need for installing additional software or infrastructure. These WebContainers are typically powered by technologies like Docker, Kubernetes, or WebAssembly, and provide a consistent and secure runtime environment for web development.

With browser-based WebContainers, developers can build, test, and deploy applications directly in the browser, without the need for setting up a local development environment or relying on cloud-based virtual machines. Instead, a WebContainer creates an isolated environment within the browser, which includes all of the dependencies, libraries, and system resources required to run an application. This environment is separate from the developer's local machine or any cloud-based infrastructure, providing a consistent and secure runtime environment for the application.

Developers can interact with the application running in the WebContainers using a web-based interface, which provides a GUI for interacting with the application and its associated files. The WebContainer API alsos provide a command-line interface for running system commands and executing code directly within the browser.

About our sample project

This article will guide you through the steps necessary to create a new Express.js application and configure it as a WebContainer for efficient deployment and management of web applications.

Whether you are an experienced web developer or just starting with WebContainers, this practical guide will provide a comprehensive understanding of how to get started with WebContainers in Node.js.

You can find the entire source code for this project on my GitHub.

Initial setup

To begin, we will create a new folder to store our project:

mkdir WebContainers

cd WebContainers
Enter fullscreen mode Exit fullscreen mode

Initialize a new npm project:

npm init
Enter fullscreen mode Exit fullscreen mode

Make sure that Node.js is installed on your system:

node -version
Enter fullscreen mode Exit fullscreen mode

If Node.js is not installed, please follow the official guide to install it or use Node version manager.

npm init is a command used to initialize a new Node.js project. When you run this command in your terminal, npm will prompt you to enter some information about your project, such as the:

  • Project name
  • Version
  • Description
  • Entry point
  • Author
  • License

Once you have provided this information, npm will generate a package.json file in your project directory with all the metadata and configuration for your project.

The package.json file is a key component of Node.js development because it contains the dependencies and scripts required to build, test, and deploy your application. By running npm init and filling out the necessary information, you can create a new Node.js project and easily manage its dependencies and configuration with npm.

Add the next content to your index.html file:

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>node.js WebContainers</title>
   <link rel="stylesheet" href="styles.css"/>
</head>
<body>
<div class="container">
   <div class="editor">
       <textarea>Editor</textarea>
   </div>
   <div class="preview">
       <iframe src="placeholder.html"></iframe>
   </div>
</div>
<div class="terminal"></div>
<script type="module" src="/index.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Add the placeholder.html file to show the initial content in iframe:

<!DOCTYPE html>
<html lang="en">
<head>
   <meta charset="UTF-8">
   <title>Loading placeholder</title>
</head>
<body>
   Installing dependencies...
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

We’ll use Vite to construct and operate our application; install it below:

npm install vite@4.1.0
Enter fullscreen mode Exit fullscreen mode

Add a script to run our application in package.json:

{
 "name": "WebContainers",
 "version": "1.0.0",
 "description": "node.js WebContainers application",
 "main": "index.js",
 "scripts": {
   "start": "vite"
 },
 "dependencies": {
   "vite": "^4.1.0"
 }
}
Enter fullscreen mode Exit fullscreen mode

Now, we can run our initial application with this command:

npm start
Enter fullscreen mode Exit fullscreen mode

You should see the simple application in your browser. The default location in the browser is http://localhost:5173. Our app in the default browser location Let's enhance the visual appeal of our application. Include the file styles.css:

* {
 box-sizing: border-box;
}

body {
 margin: 0;
 height: 100vh;
}

.container {
 display: grid;
 grid-template-columns: 1fr 1fr;
 gap: 1rem;
 height: 50%;
 width: 100%;
}

textarea {
 width: 100%;
 height: 100%;
 resize: none;
 border-radius: 0.5rem;
 background: black;
 color: greenyellow;
 padding: 0.5rem 1rem;
}

iframe {
 height: 100%;
 width: 100%;
 border-radius: 0.5rem;
}
Enter fullscreen mode Exit fullscreen mode

Restart the application; you should see the following: Restart the app to see our styling

Setting up WebContainers with Express.js

Express.js is a web application framework for Node.js that allows developers to build fast and scalable web applications with features such as middleware, routing, and templating engines. It is simple and flexible, making it a popular choice among developers.

For demonstration purposes, I have integrated it within our app, but in the actual production environment, our Express.js application will be separate from our main application. Later, you can connect any Express.js application to WebContainers.

Creating the Express.js application

Create a new folder called app:

mkdir app
cd app
Enter fullscreen mode Exit fullscreen mode

Inside the app folder, we need two files: index.js and package.json. index.js is a straightforward Express.js application that returns “Hello World” text on the root route:

import express from 'express';
const app = express();
const port = 3111;

app.get('/', (req, res) => {
 res.send('Hello World!');
});

app.listen(port, () => {
 console.log(`The application can be accessed at http://localhost:${port}, as it is now live.`);
});
Enter fullscreen mode Exit fullscreen mode

To run the application, we should install dependencies. Add the following in the app/package.json file:

{
 "name": "example-app",
 "type": "module",
 "dependencies": {
   "express": "latest",
   "nodemon": "latest"
 },
 "scripts": {
   "start": "nodemon --watch './' index.js"
 }
}
Enter fullscreen mode Exit fullscreen mode

And install your dependencies inside the app folder:

npm install
Enter fullscreen mode Exit fullscreen mode

Now, you can run your Express.js application with the following command:

npm start
Enter fullscreen mode Exit fullscreen mode

And see the result on http://localhost:3111: Running our Express.js application

Integrating WebContainers

Once we are done with the Express.js application, we can move back to the root folder and continue working on WebContainers:

cd ..
Enter fullscreen mode Exit fullscreen mode

First things first, let’s install WebContainers in our application:

npm install @webcontainer/api@1.0.2
Enter fullscreen mode Exit fullscreen mode

We'll create a file named index.js, where we declare constants to manipulate HTML elements and create a WebContainers instance:

import {WebContainer} from '@webcontainer/api'

const iframe = document.querySelector('iframe')
const textarea = document.querySelector('textarea')

let WebContainersInstance
Enter fullscreen mode Exit fullscreen mode

Add a load event listener for the window object:

window.addEventListener('load', async () => {
   console.log('Window is loaded')
})
Enter fullscreen mode Exit fullscreen mode

Creating a file autoloader

Let's create a file autoloader to ensure that our Express.js files are loaded into our WebContainers.

Create a new file autoloader.js and put the following content inside:

const fs = require('fs')

const INPUT = 'app'
const OUTPUT = 'files.js'
// List of files to include into WebContainers
const files = ['index.js', 'package.json']
const exportLine = 'export const files = '

const content = {}

files.forEach(file => {
   const buffer = fs.readFileSync(`./${INPUT}/${file}`)
   content[file] = {
       file: {
           contents: buffer.toString()
       }
   }
})

fs.writeFileSync(OUTPUT, `${exportLine}${JSON.stringify(content, null, 2)}`)
Enter fullscreen mode Exit fullscreen mode

It is advisable to prepare our scripts so that this code is executed prior to commencing project. Change the script section in the package.json file:

"scripts": {
 "start": "npm run prepare && vite",
 "prepare": "node ./autoloader.js"
},
Enter fullscreen mode Exit fullscreen mode

The last changes should be done in the index.js file: import the data from the created file and attach it to our editor window:

// …
import {files} from "./files"

// …

window.addEventListener('load', async () => {
   textarea.value = files['index.js'].file.contents

   console.log('Window is loaded')
})
Enter fullscreen mode Exit fullscreen mode

Now you can run the application:

npm start
Enter fullscreen mode Exit fullscreen mode

Make sure that the content from your app/index.js appears in the editor window.

Running WebContainers

Now, we will instantiate a new WebContainers instance:

window.addEventListener('load', async () => {
   textarea.value = files['index.js'].file.contents

   WebContainersInstance = await WebContainer.boot()
   await WebContainersInstance.mount(files)

   console.log('Window is loaded')
})
Enter fullscreen mode Exit fullscreen mode

With most popular browsers, you may meet an error with a blocking frame. We should include headers to match the Vite configuration. Create a vite.config.js file with the next content:

export default {
   server: {
       port: 5173,
       headers: {
           'Cross-Origin-Embedder-Policy': 'require-corp',
           'Cross-Origin-Opener-Policy': 'same-origin',
       },
   }
}
Enter fullscreen mode Exit fullscreen mode

Then, hard reload the page (Control+Command+R on macOS, Control+Shift+R on Windows) to prevent caching and restart the Vite server.

Installing dependencies

It is time to install our Express.js project dependencies inside our WebContainer. Add another function to the index.js file:

async function installDependencies() {
   const installProcess = await WebContainersInstance.spawn('npm', ['install'])

   installProcess.output.pipeTo(new WritableStream({
       write(data) {
          console.log(data)
       }
   }))

   return installProcess.exit
}
Enter fullscreen mode Exit fullscreen mode

Run this function in the load callback:

window.addEventListener('load', async () => {
   textarea.value = files['index.js'].file.contents

   WebContainersInstance = await WebContainer.boot()
   await WebContainersInstance.mount(files)

   await installDependencies()

   console.log('Window is loaded')
})
Enter fullscreen mode Exit fullscreen mode

After running the application, you can see the entire dependency installation process within the browser’s console.

Running an Express.js service inside WebContainers

We are ready to run the Express.js application inside WebContainers. Add a function in the index.js file:

async function startDevServer() {
   const serverProcess = await WebContainersInstance.spawn('npm', ['run', 'start'])

   serverProcess.output.pipeTo(new WritableStream({
         write(data) {
             console.log(data)
         }
     }))

   WebContainersInstance.on('server-ready', (port, url) => {
       iframe.src = url
   })
}
Enter fullscreen mode Exit fullscreen mode

Execute this function inside the load callback:

window.addEventListener('load', async () => {
   textarea.value = files['index.js'].file.contents

   WebContainersInstance = await WebContainer.boot()
   await WebContainersInstance.mount(files)

   await installDependencies()

   startDevServer()

   console.log('Window is loaded')
})
Enter fullscreen mode Exit fullscreen mode

After all dependencies are installed you can see the express.js app response in the iframe window.

Editing the Express.js application inside the WebContainer

Merely reading the Express.js application would be futile; we should have the ability to modify it. Add a function into index.js file:

async function writeIndexJS(file, content) {
   await WebContainersInstance.fs.writeFile(`/${file}`, content)
}
Enter fullscreen mode Exit fullscreen mode

We would like to catch all input events from the editor window and reflect them in the iframe window. Add an event listener inside the load callback:

window.addEventListener('load', async () => {
   textarea.value = files['index.js'].file.contents

   textarea.addEventListener('input', (e) => {
       writeIndexJS('index.js', e.currentTarget.value)
   })

   // …
})
Enter fullscreen mode Exit fullscreen mode

Adding pretty logging with xterm

In essence, we have integrated our basic Express.js application into a WebContainer. However, we aim to incorporate elegant logging instead of relying on the console within the browser. This will provide a more convenient way to access log files and dig into the complete dependency installation process when needed.

Let’s install a new dependency:

npm install xterm@5.1.0
Enter fullscreen mode Exit fullscreen mode

xterm is a JavaScript library that provides a web-based terminal emulator with ANSI escape sequences, Unicode characters, and other features. It is easy to use and customize, making it a popular choice for adding a terminal interface to web applications.

We have to capture the HTML element with the class terminal and attach the terminal to it. Import xterm and its styles into index.js:

//…
import {Terminal} from 'xterm'
import 'xterm/css/xterm.css'
//…
Enter fullscreen mode Exit fullscreen mode

And add a variable for terminal HTML element:

const iframe = document.querySelector('iframe')
const textarea = document.querySelector('textarea')
const terminalElement = document.querySelector('.terminal')
Enter fullscreen mode Exit fullscreen mode

Initialize the terminal in the load callback:

window.addEventListener('load', async () => {
   //…

   const terminal = new Terminal({
       convertEol: true,
   })
   terminal.open(terminalElement)

   //…
})
Enter fullscreen mode Exit fullscreen mode

We have to pass the terminal instance in all functions where we need output and replace the console.log. Refactor the installDependencies function:

async function installDependencies(terminal) {
   const installProcess = await WebContainersInstance.spawn('npm', ['install'])

   installProcess.output.pipeTo(new WritableStream({
       write(data) {
           terminal.write(data)
       }
   }))

   return installProcess.exit
}
Enter fullscreen mode Exit fullscreen mode

The same for the startDevServer function:

async function startDevServer(terminal) {
   const serverProcess = await WebContainersInstance.spawn('npm', ['run', 'start'])

   serverProcess.output.pipeTo(new WritableStream({
       write(data) {
           terminal.write(data)
       }
   }))

   WebContainersInstance.on('server-ready', (port, url) => {
       iframe.src = url
   })
}
Enter fullscreen mode Exit fullscreen mode

Pass the terminal instance to the functions in the load callback:

await installDependencies(terminal)

startDevServer(terminal)
Enter fullscreen mode Exit fullscreen mode

Restart the application. The end result should look like this: Our final app running in the browser The entire source code can be found on my GitHub.

Conclusion

Browser-based WebContainers are a powerful tool for web developers when combined with Node.js, especially if you’re looking to streamline your development process and improve your workflow. These containers offer a lightweight and scalable solution for building and deploying web applications, allowing developers to easily test and deploy their code in a secure and isolated environment.

As Node.js remains popular in web development, browser-based WebContainers provide an innovative and efficient way to leverage its capabilities in new settings. By incorporating this technology into your workflow, you can enhance your productivity and create high-performance web applications with ease.

Overall, browser-based WebContainers with Node.js are a valuable addition to any web development toolkit, and their potential for innovation and optimization is only set to increase in the future.


Get setup with LogRocket's modern Node error tracking in minutes:

1.Visit https://logrocket.com/signup/ to get an app ID.
2.Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.

NPM:

$ npm i --save logrocket 

// Code:

import LogRocket from 'logrocket'; 
LogRocket.init('app/id');
Enter fullscreen mode Exit fullscreen mode

Script Tag:

Add to your HTML:

<script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
<script>window.LogRocket && window.LogRocket.init('app/id');</script>
Enter fullscreen mode Exit fullscreen mode

3.(Optional) Install plugins for deeper integrations with your stack:

  • Redux middleware
  • ngrx middleware
  • Vuex plugin

Get started now

Top comments (0)