Over the years I've built many email solutions that were simply meant to send emails to myself. Whether it be for notifying myself of errors, users reporting bugs, requests for information, or just general contact, the process was always relatively the same, and I always wished there was a simpler solution. Turns out, there IS one!
I first heard about it on Syntax, and then again on JS Party. With it being featured on 2 of my fav podcasts, I thought I should give it a look, and boy was I glad I did!
What is this amazing solution you ask? Well it's a nifty little tool called Val Town.
Val Town is a website where you can write, run, and deploy small snippets of Javascript or Typescript, which are called Vals. If you're familiar with GitHub Gists, you can think of these very much the same, except Vals can be run and hosted like an API! I'll show an example of this a little later.
Immediately after signing up, right there in their intro tutorial was the answer to my woes. A simple, built in function for sending yourself an email!
As luck would have it, I'm currently in the middle of rebuilding my portfolio site, and naturally I need a "Get in Touch" section. I wasn't really looking forward to building this functionality out again, so of course I HAD to give this new tool a try. Turns out, it's pretty awesome! And that's what this post is going to be about.
I'm going to show just how easy Val Town was to set up and implement with an external project. I built my solution in SvelteKit, but since this can all be implemented across any stack, I'm going to try and make it as agnostic as I can. But since I do have to choose a language to write it in, I'm going to show this to you in Javascript.
Overview
Let's first have a look at what we are going to build.
As you can see, we're going to create a very simple flow where our Client is going to be able to send data to our own API. The API will then send the data to Val Town where it will be emailed to yourself. Val Town will then send a response to the API letting it know if it was successful or not. The API will then relay this information back to the Client to let the user know.
We're going to start in Val Town where we'll create our Val. Then we'll move into our API which will receive some form data, parse it, and send it to Val Town. Lastly, we'll build a simple form that will submit data to our API.
Now that we know what we are going to be building, let's hop in the car and head over to Val Town!
Welcome to Val Town!
If you haven't already, go ahead and create your Val Town account. Make sure you use the email address that you want to send emails to. I also recommend going through their short tutorial to get a sense of how stuff works.
Now, go to Home. You may already see a few Vals displayed here if you did the tutorial, but you can ignore them for now. Next to your avatar in the top right of the window click "New Val", which should open a modal that looks like this:
This is where we're going to write our first bit of Javascript to handle emailing ourselves. Our code will need to take 2 parameters, which are both strings, subject
and html
. subject
will be the subject of the email being sent, and html
will be the (HTML optional) body of the email. We'll then use the built in function to send an email to ourselves, and lastly respond with the status.
function sendEmail(subject, html) {
try {
console.email({ html, subject });
return Response.json({ status: "ok" });
} catch (err) {
return Response.json({ status: "error", error: err.message });
}
}
Now click Run
.
You should now see your Val displayed at the top of your list of Vals on your homepage.
I want to point out a couple of things in the header of your new Val...
Notice the name of the Val has been appended to your @username. This is how you will reference the Val later.
Next to the Val name you should see a version (v0). If you edit the Val and then click
Save
, Val Town will automatically create a new version of your Val for you. If you click on this version number, a dropdown will appear that will let you pick from past versions if you ever want to revert back.Next, you should see a lock icon? This means your Val is currently private. So right now only you and your other Vals can access it. If you click this icon, you'll be presented with options to
Unlist
it, or make itPublic
. For this tutorial, I recommend keeping it set to private.Then all the way to the right of the Val header, you should see an options menu button (it has three horizontal dots). If you press this, you'll be presented with a dropdown that has a whole bunch of options. I want to draw your attention to the
Endpoints
option. If you hover over this, you'll see another menu appear that will let you copy either a run or express fetch. I point this out because we will make use of this in a later step.
But that's it! Your Val is now set up and ready to use! Simple, right?!
We'll come back to Val Town in the next section when we need to get our API key and to copy the fetch request code. But for now, let's move on to the API.
The API
To keep things fairly general, I'm going to set up a very simple API here using ExpressJS.
Let's start by setting up the new project. I'll call mine "email-yo-self".
(ℹ️ If you'd rather just look at the project code without reading through the following setup, you can find it here);
- create the project directory by running
mkdir email-yo-self && cd email-yo-self
- initialize the project with npm defaults by running
npm init -y
- initialize the version control system (VCS) by running
git init
- we don't want all the files to be tracked by git, so create a new ".gitignore" file and add the following content to it:
node_modules
.env
- install the required dependencies by running
npm i express dotenv body-parser
-
express
for the api -
dotenv
to inject the environment variables -
body-parser
to parse incoming request bodies
-
- install nodemon as a dev dependency to restart the script when you save changes by running
npm i -D nodemon
- create a new
.env
file in the root of the project and add the following contents to it:
PORT=80 # or whatever port you wish to use
VAL_TOWN_API_TOKEN=
- lastly, add the following start script to the
package.json
{
...
"scripts": {
"start": "nodemon index.js",
...
},
...
}
🗒️ In a normal project, there should be more setup, such as adding linting with eslint, and/or testing using something like Jest. But we're going to skip that here to keep things short.
Alright, time to write some code.
There are a couple steps here, and we'll take them 1 at a time.
First, let's set up our base express app, with a single endpoint we'll use to test that everything's running okay...
const express = require('express');
const dotenv = require('dotenv');
const bodyParser = require('body-parser');
dotenv.config();
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.get('/status', (req, res) => {
res
.status(200)
.json({ status: 'ok' });
});
app.listen(process.env.PORT, () => {
console.log(`express app listening on port ${process.env.PORT}`);
});
Alright, let's test that our API is working as expected. In your terminal, run npm start
, then visit http://localhost:80
in your browser where you should see {status: "ok"}
printed.
The POST Request
Awesome! Our API is up and running. Now let's add the POST route that will handle receiving some form data.
app.post('/', async (req, res) => {
const { subject, body } = req.body;
if (!subject) {
return res
.status(422)
.json({ error: 'A subject is required.' });
}
if (!body) {
return res
.status(422)
.json({ error: 'A message body is required.' });
}
res
.status(200)
.json({ message: 'Everything looks good so far...' });
});
🗒️ For production, you will want to perform additional validation on the subject
and body
values to make sure the user isn't trying to send you malicious content. This is beyond the scope of this post, but there are many tools out there that can help with this, like sanitize-html for example.
Now that we have more than a simple GET request, we're going to need something more to test our new endpoint. I personally prefer to use Postman. So we'll add the request to Postman and test that everything is working.
Great! Our POST endpoint is receiving the form data successfully. We're finally ready to make our request to Val Town.
First, go ahead and replace the 200 response we were using for testing with an empty try...catch
block. Your endpoint should look like this...
app.post('/', async (req, res) => {
const { subject, body } = req.body;
if (!subject) {
return res
.status(422)
.json({ error: 'A subject is required.' });
}
if (!body) {
return res
.status(422)
.json({ error: 'A message body is required.' });
}
try {
} catch (err) {
}
});
Now, if you recall, earlier I mentioned the privacy setting on the Val we created. Since we left it as Private, we have to provide an API Token in order to access it. Lucky for us, Val Town makes this super easy.
Go back to your Val Town homepage and click your username in the upper right of the window. In the dropdown that appears, click "API tokens".
This will take you to another page where you should see at least 1 token already created for you. If you don't see one (it will look something like this: abcd1234-a123-123a-456b-123ab45c67de
(<- this is just an example and is not a real token)) just click the "New" button and a new token will be generated for you.
Copy the token and paste it inside your .env file as the value for the VAL_TOWN_API_TOKEN
environment variable we added earlier.
Your .env file should now look something like this...
PORT=80
VAL_TOWN_API_TOKEN=abcd1234-a123-123a-456b-123ab45c67de
Next, let's add the fetch request to our code. Remember the more options menu in the header of your Val that I pointed out earlier? Go back there, hover over the "Endpoints" option and click "Copy run fetch".
Go back to the POST endpoint inside the index.js file, paste the fetch request into your try
block.
🚨 IMPORTANT 🚨
Take a look at the code you just pasted. Notice there's a token included in it. THIS NEEDS TO BE REMOVED! To do this, update your headers
object so it looks like this...
...
headers: { Authorization: `Bearer ${process.env.VAL_TOWN_API_TOKEN}` },
...
Next, if the request is successful, we'll want to send a success response. So just below the fetch request, add the following:
return res
.status(200)
.send(`
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<p>Your email was sent successfully!
</body>
</html>`);
We also need to make sure to handle any errors, so inside the empty catch block, add something like this...
console.log(err);
return res
.status(500)
.json({ error: 'Well this is awkward. Something seems to be have gone wrong while sending your email. Please try again later.' });
Your endpoint should now look like this...
app.post('/', async (req, res) => {
const { subject, body } = req.body;
if (!subject) {
return res
.status(422)
.json({ error: 'A subject is required.' });
}
if (!body) {
return res
.status(422)
.json({ error: 'A message body is required.' });
}
try {
await fetch("https://api.val.town/v1/run/jakelundberg.sendEmail", {
method: "POST",
body: JSON.stringify({args: [subject, body]}),
headers: { Authorization: `Bearer ${process.env.VAL_TOWN_API_TOKEN}` },
});
return res
.status(200)
.send(`
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<p>Your email was sent successfully!
</body>
</html>`);
} catch (err) {
console.log(err);
return res
.status(500)
.json({ error: 'Well this is awkward. Something seems to be have gone wrong while sending your email. Please try again later.' });
}
});
Lastly, let's test the endpoint! You'll need to restart your script (if you haven't already) since you made changes to your .env file. Once you've done that, resubmit your request in Postman (or whatever tool you are using).
If everything was successful, not only should you have gotten a successful response, but after a moment or 2, you should also receive an email from yourself!!!
And that's it! Our POST request is done!
The Client
The client for this project is going to be kept very simple, using plain ol' HTML.
We already have our server set up, so we're just going to create an endpoint that will deliver our form to the browser.
app.get('/', (req, res) => {
res
.status(200)
.send(`
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head>
<body>
<form method="post">
<div>
<label for="subject">Subject</label>
<input type="text" name="subject" id="subject" />
</div>
<div>
<label for="body">Body</label>
<textarea name="body" id="body"></textarea>
</div>
<button>Send</button>
</form>
</body>
</html>`);
});
Now, if you visit http://localhost:80
in your browser, you should see the barebones form. And if you fill it out and submit it, you should receive an email in just a moment or 2!
Conclusion
Let's recap everything we just did.
We created a Val in Val Town that handled sending ourselves an email using their built in console.email
function. We then created an API that was able to send data to that Val. Lastly we added an HTML form to kick the whole process off!
You can find all the code we wrote in the associated GitHub repo!
I hope this post sheds a little light on how amazing Val Town is, and how you can do some pretty impressive stuff with a very small amount of code!
Thanks for taking the time to discover something new with me, and until next time, Happy Hacking!