Setup Full-Stack Webapp
In this tutorial we're going to build a webapp that tells users a random joke from a database, and let's them submit their own jokes to it.
Prerequisites[edit | edit source]
First, open Git Bash. If you don't have Git Bash, get it from https://git-scm.com/
We also need to make sure we have NodeJS installed. We can test this by running this command in Git Bash:
npm --version
It should give something like 10.2.3
. If not get it from https://nodejs.org/
The final tool we need is the Merrymake CLI, which uses Git and NodeJS. Install the CLI with the command:
npm install --global @merrymake/cli
Start Using Merrymake[edit | edit source]
With the CLI installed, we are ready to start using Merrymake. The first command we should run on every device we want to use Merrymake on is:
mm start
The mm
tool now guides us through the initial process. Select:
setup new key specifically for Merrymake
- Input your email address
create new organization
- Name your organization or press enter to accept the suggested name
- Press enter to accept the suggested service group name (service-group-1)
- Press enter to accept the suggested repository name (repo-1)
initialize with a basic template
typescript
deploy the service immediately
post an event to the rapids
- Press enter to accept the suggested event type (hello)
attach plain text payload
- Input your name
- Input "admin key"
- Press enter to accept the suggested duration (14 days)
We should now see Hello, XXXX!
, this means our service is running in the cloud. If you don't see this please reach out on our Discord to get assistance.
Connect a Database[edit | edit source]
We are ready to add a database to our backend. We navigate into the repository we just created with the command:
cd [organization name]/service-group-1/repo-1
Head over to mongodb and create a free MongoDB database. We need to allow all IPs to be able to access the cluster so head to Network Access under the Security tab. Add a new IP and allow access from everywhere. Head back to the database and copy the connection string. We put this URI in a secret environment variable with the command:
mm envvar
- Input
DB
the value is secret
- Insert the connection string
accessible in both prod and init run
To work with the database we need to install some npm packages for it:
npm install mongodb
Setup collections in the init run[edit | edit source]
The init run is executed exactly once, on deployment. Therefore, it is perfect for setting up database collections. We need one documentjoke
with one attributejoke
. Open the file index.ts
and replace its content with this:
Before | After |
---|---|
import {
merrymakeService,
MIME_TYPES,
replyToOrigin,
} from "@merrymake/service";
async function handleHello(payloadBuffer: Buffer) {
let payload = payloadBuffer.toString();
replyToOrigin(`Hello, ${payload}!`, MIME_TYPES.txt);
}
merrymakeService({
handleHello,
});
|
import {
merrymakeService,
MIME_TYPES,
replyToOrigin,
} from "@merrymake/service";
import { Client } from "pg";
async function handleHello(payloadBuffer: Buffer) {
let payload = payloadBuffer.toString();
replyToOrigin(`Hello, ${payload}!`, MIME_TYPES.txt);
}
merrymakeService(
{
handleHello,
},
async () => {
if (process.env.DB) {
const client = new MongoClient(process.env.DB);
try {
await client.connect();
await client.db("Merrymake").createCollection("joke").then();
} finally {
await client.close().then();
}
} else {
console.log("Environment variable DB is not defined");
}
}
);
|
Implement the functionality[edit | edit source]
Our backend will have three operations: retrieveOne, retrieveAll, and insert. First we need to setup hooks so we can eventually trigger the functions, so go into the merrymake.json
file and replace its content with this:
Before | After |
---|---|
{
"hooks": {
"main/hello": "handleHello"
}
}
|
{
"hooks": {
"main/retrieveOne": "handleRetrieveOne",
"main/retrieveAll": "handleRetrieveAll",
"main/insert": "handleInsert"
}
}
|
Then, we replace the handleHello
function with three new functions index.ts
Before | After |
---|---|
import {
merrymakeService,
MIME_TYPES,
replyToOrigin,
} from "@merrymake/service";
import { Client } from "pg";
async function handleHello(payloadBuffer: Buffer) {
let payload = payloadBuffer.toString();
replyToOrigin(`Hello, ${payload}!`, MIME_TYPES.txt);
}
merrymakeService(
{
handleHello,
},
async () => {
const client = new Client({ connectionString: process.env.DB });
try {
await client.connect();
await client.query(`CREATE TABLE IF NOT EXISTS joke ()`);
await client.query(`ALTER TABLE joke
ADD COLUMN IF NOT EXISTS joke VARCHAR`);
} finally {
client.end();
}
}
);
|
import {
merrymakeService,
MIME_TYPES,
replyToOrigin,
} from "@merrymake/service";
import { MongoClient } from "mongodb";
async function handleRetrieveOne(payloadBuffer: Buffer) {
if (process.env.DB) {
const client = new MongoClient(process.env.DB);
try {
await client.connect().then();
const row = await client
.db("Merrymake")
.collection("joke")
.aggregate([{ $sample: { size: 1 } }])
.toArray()
.then();
replyToOrigin(JSON.stringify(row), MIME_TYPES.json);
} finally {
await client.close().then();
}
} else {
console.log("Environment variable DB is not defined");
}
}
async function handleRetrieveAll(payloadBuffer: Buffer) {
if (process.env.DB) {
const client = new MongoClient(process.env.DB);
try {
await client.connect().then();
const row = await client
.db("Merrymake")
.collection("joke")
.find()
.toArray()
.then();
replyToOrigin(JSON.stringify(row), MIME_TYPES.json);
} finally {
await client.close().then();
}
} else {
console.log("Environment variable DB is not defined");
}
}
async function handleInsert(payloadBuffer: Buffer) {
const payload = payloadBuffer.toString().trim().replace("\\n", "<br>");
const input = {
joke: payload,
};
if (process.env.DB) {
const client = new MongoClient(process.env.DB);
try {
await client.connect().then();
await client.db("Merrymake").collection("joke").insertOne(input);
replyToOrigin("OK", MIME_TYPES.txt);
} finally {
await client.close().then();
}
} else {
console.log("Environment variable DB is not defined");
}
}
merrymakeService(
{
handleRetrieveOne,
handleRetrieveAll,
handleInsert,
},
async () => {
if (process.env.DB) {
const client = new MongoClient(process.env.DB);
try {
await client.connect();
await client.db("Merrymake").createCollection("joke").then();
} finally {
await client.close().then();
}
} else {
console.log("Environment variable DB is not defined");
}
}
);
|
Now to deploy the service we simply run:
mm deploy
The Merrymake platform automatically builds, packages, and deploys the service, before executing the init-run, and then hooks it up for traffic.
Interacting with the Rapids[edit | edit source]
Every execution of our service is triggered by an event on a large queue called the Rapids. We can see the status of executions with the command:
mm rapids
From here we can see that we have an init
event which was successful. We can also drill into specific events to get details, such as what they printed to stdout.
We can also post events to the rapids, to trigger services. Let's try adding a joke by triggering the insert event:
post message to rapids using an api-key
- Input
insert
attach plain text payload
- Input
Why did the chicken cross the road?\nTo get to the other side
admin-key (XXXX)
We should see Ok
. To verify that it worked we can trigger the retrieveOne like this:
mm rapids post retrieveOne empty _
Which should return our singular joke from above.
Add a Front-End[edit | edit source]
In Merrymake we get a free public front-end, which we can manage from the public folder, let's navigate to it:
cd ../../public
Replace the content of the index.html
file
Before | After |
---|---|
<html><body>Hello, World!</body></html>
|
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dad jokes</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<p id="show-joke"><i class="fa fa-spinner fa-pulse"></i></p>
<div class="spacer"></div>
<form id="new-joke">
<h1>Submit a joke</h1>
<p><textarea id="joke" rows=3></textarea></p>
<input type="submit" value="Submit!" />
</form>
<div id="thank-you">That's really funny 😂</div>
<script>
// More to come here
</script>
</body>
</html>
|
Also create a new file style.css
body,
textarea {
text-align: center;
line-height: 1.8em;
}
h1 {
font-size: 40px;
}
body,
.spacer {
margin-top: 100px;
}
body,
textarea,
input {
font-family: "Gill Sans", "Gill Sans MT", Calibri, "Trebuchet MS", sans-serif;
font-size: 27px;
}
textarea,
input {
border-radius: 5px;
}
textarea {
width: 800px;
}
input[type="submit"] {
padding: 10px 20px;
background-color: #dfd;
}
#thank-you {
display: none;
}
We can open up the index.html in a browser to see how our front-end looks and behaves. Currently it only shows an infinite loading spinner. So let's add some functionality.
Get a joke from the backend[edit | edit source]
We've see how we can trigger the backend from the CLI, during which we setup an "admin key". We can also trigger services via an HTTP request using an API key. It is best practice to use different keys for each source, so we create another key with:
mm key
add a new apikey
- Input
front-end
- Input
1 year
It then gives you a key, highlighted in yellow, keep this close at hand.
Using this key we can add code in the script
block to retrieve one joke on startup and replace the spinner with it.
Before | After |
---|---|
<script>
// More to come here
</script>
|
<script>
(async () => {
let resp = await fetch("https://rapids.merrymake.io/[YOUR KEY]/retrieveOne");
let text = await resp.text();
document.getElementById("show-joke").innerHTML = text;
// Even more to come
})();
</script>
|
As we can see to trigger an event we use the URL https://rapids.merrymake.io/[APIKEY]/[EVENT]
Send a joke to the backend[edit | edit source]
The only thing missing from our app is the ability for users to add their own jokes. This works similarly but here we use a POST method, with a content type, and we pass the API key in the header.
Before | After |
---|---|
<script>
(async () => {
...
// Even more to come
})();
</script>
|
<script>
(async () => {
...
let elem = document.getElementById("new-joke");
elem.addEventListener("submit", async (e) => {
e.preventDefault();
let resp = await fetch("https://rapids.merrymake.io/insert", {
method: "POST",
headers: {
"Content-type": "plain/text",
"merrymake-key": "[YOUR KEY]"
},
body: document.getElementById("joke").value
});
elem.style.display = "none";
document.getElementById("thank-you").style.display = "block";
});
})();
</script>
|
We can test our app locally, and when we are happy with it we can deploy it with:
mm deploy
Our app is now available to everyone on https://[org].merrymake.app.
Secure the App[edit | edit source]
Currently both our keys are universal keys, that means they can trigger any event. However, the front-end only needs to trigger the insert
and retrieveOne
events, so let's limit front-end
key to these events.
Declare events in the event-catalogue[edit | edit source]
To limit access we need to first declare the events in the event-catalogue. We do this by going into the event-catalogue folder
cd ../../event-catalogue
Here we can replace the content of api.json
Before | After |
---|---|
{
}
|
{
"insert": {},
"retrieveOne": { "waitFor": 2000 }
}
|
We also configure how long to wait for before timing out and sending back "Queued event". Save the file and deploy the event-catalogue:
mm deploy
Allowlist the events[edit | edit source]
The final step is to specify that only these events are allowed on the front-end
key, we do this with
mm event
front-end (XXXXX)
- Then check
insert
andretrieveOne
Notice we cannot allow the event retrieveAll
because we didn't declare it in the event-catalogue.
We can verify that it worked by trying to trigger the retrieveAll
on the front-end key with:
mm rapids post retrieveAll empty
You can also try the admin key
which is still a universal key, thus it can trigger any event.
Conclusion[edit | edit source]
Using the CLI we have:
- Registered an account and created an organization
- Deployed a backend, the public front-end, and the event-catalogue
- Inspected our Rapids
- Posted events with and without payload to our Rapids
- Set a secret environment variable
- Created two API keys
- Allow-listed events on the front-end key
We have seen how to implement and deploy a serverless backend with a database and three hooks.
We have deployed a public front-end, which we have also secured.
These are the basic steps to launch a full-stack webapp in Merrymake.
Thank you for your time.