Continuing Frameworkless.js
Part 6: Emails, cronjobs & deploying!

Recap of Last Time
- We built a Node.js app utilising the built-in http.createServer
- Starts an HTTP server and serves static files out of the ./public directory
- We implemented dynamic routing with a handy easy to define syntax
- Our routes use Handlebars template files to display content that's not static
- We also wrote a super simple form data parser that will allow us to capture user data
- We connected up a database, using SQL queries mixed into "actions" to contain them
- We made a pipeline making it easier for us to keep our frontends nice
What are we building again?
Ever wanted to remind yourself of something, out of the blue? Well, we're building a quick tool to do that!
What's it all for?

- To start, this course was to prove that
 you don't need a framework.
- Sometimes, not thinking properly about your architectural choices can lead you down a wrong path. Always consider the true cost of a framework!
- Don't avoid frameworks just because either – they serve a very useful purpose.
- An MVP should be simple. Simplify your MVP build by focusing away from learning/working in frameworks!
What we're going to do
- We need to connect our app to Mailgun.
- A background task (cronjob) needs to be made to run our app's sender.
- We deploy to Heroku!
Let's get started with the mailer
We're going to use Mailgun, but any transactional provider will do.
$ npm i --save mailgun-js- You will need to go to mailgun.com, create an account.
- You will also probably need to set up the MX/DKIM records on a domain name you own
We're already using Handlebars...
We're already using Handlebars.js in our app's views, so may as well use it in emails too!

(reduce, re-use, recycle)
$ mkdir emails
$ touch index.js
$ touch reminder.hbsLet's create our email
{{!-- emails/reminder.hbs --}}
🎯 REMINDER from {{timeago reminder.created_at}}
---
<p>Hi there, {{email}}!</p>
<p>{{message}}</p>
<p><small>You are receiving this email because you asked <a href="https://remind.ist?ref=email">{{app_name}}</a> to remind you! Now that this message is sent, it has self-destructed in our database.</small></p>
Emails: Loading the files in
// emails/index.js
const { readFileSync } = require('fs')
const Handlebars = require('handlebars')
const glob = require('glob')
const hbsHelpers = require('../lib/handlebar-helpers')
Object.keys(hbsHelpers).map(helper => {
  Handlebars.registerHelper(helper, hbsHelpers[helper])
})
const mails = {}
for (const mail of glob.sync('./emails/**.hbs')) {
  const pathTokens = mail.split('/')
  const key = pathTokens[pathTokens.length - 1].slice(0, -4)
  const [ subject, body ] = readFileSync(mail, { encoding: 'utf8' }).split('---')
  mails[key] = { subject, body }
}Emails: the compile function
// emails/index.js
const compile = (emailName, context) => {
  const mail = mails[emailName]
  if (!mail) throw new Error(`Email ${emailName} not found.`)
  const body = Handlebars.compile(mail.body)
  const subjectTemplate = Handlebars.compile(mail.subject)
  const from = MAILGUN_SEND_EMAIL_FROM || `Remind.ist <bot@${MAILGUN_DOMAIN}>`
  const compileContext = {
    app_name: APP_NAME,
    ...context
  }
  const subject = subjectTemplate(compileContext)
  const message = {
    from,
    to: context.email,
    subject,
    html: body({ ...compileContext, subject }),
    'o:tag': [ emailName ],
    'o:tracking': 'True'
  }
  return message
}Emails: sending via mailgun
// emails/index.js
// Near the top...
const MailgunClient = require('mailgun-js')
const { MAILGUN_API_KEY, MAILGUN_DOMAIN, APP_NAME, MAILGUN_SEND_EMAIL_FROM } = process.env
if (!MAILGUN_API_KEY || !MAILGUN_DOMAIN) throw new Error(
  'MAILGUN_API_KEY & MAILGUN_DOMAIN are required env variables.'
)
const client = MailgunClient({
  apiKey: MAILGUN_API_KEY,
  domain: MAILGUN_DOMAIN,
  username: 'api'
})
// Anywhere, really...
const send = async email => {
  try {
    const send = await client.messages().send(email)
    console.log(`=> Email queued: ${send.id}`)
  } catch (error) {
    console.error(`=> Error sending email: ${error.message}`)
    if (error.stack) console.log(error.stack)
  }
}
// End
module.exports = { compile, send }Now, we need a way to send them...
$ mkdir bin/reminders
$ touch bin/reminders/send
$ chmod +x bin/reminders/sendA daily task designed to be run in the server environment will send the mails for us by using our compile and send functions!
The magic task...
#!/usr/bin/env node
if (!process.env.DATABASE_URL) require('dotenv').config()
const db = require('../../initialisers/postgres')
const { compile, send } = require('../../emails')
const run = () => new Promise(async resolve => {
  const now = new Date()
  now.setUTCHours(0)
  now.setUTCMinutes(0)
  now.setUTCSeconds(0)
  now.setUTCMilliseconds(0)
  console.log(`=> Running mailer job for ${now.toISOString()}`)
  const { rows: reminders } = await db.query(
    'SELECT * FROM reminders WHERE send_at = $1',
    [ now ]
  )
  console.log(`  ${reminders.length} outstanding emails found.`)
  if (reminders.length < 1) {
    console.log(`  Zoinks – no emails need sending, let's get out of here!`)
    return resolve()
  }
  for (const reminder of reminders) {
    const message = await compile('reminder', reminder)
    await send(message)
    await db.query('DELETE FROM reminders WHERE reminder_id = $1', [ reminder.reminder_id ])
  }
  return resolve(0)
})
run()
  .then(process.exit)
  .catch(error => {
    console.error(error)
    process.exit(2)
  })
Time to execute the task
$ bin/reminders/sendA "cronjob" is the simplest way of doing this:
0 9 * * *    /path_to_app/bin/reminders/send* Or, we could use Heroku's scheduler
What is heroku?
- Heroku is a hosting platform. Their hosting has an emphasis on the 12 factor app, so it's perfect for us!
- They have a free tier so you can tinker all you want before launch.
- They have a number of free addons built in that make your life easier.
- In the long run though, if you want to learn to do deployments on your own or if cost is a factor, you will want to migrate away from Heroku, especially at scale.
Heroku deployment checklist
- Create a Heroku account
- Create an app
- Provision logging, a Postgres database and a Heroku Scheduler
- If your code is on Github, sync the repo with the app, otherwise use Heroku's CLI
- Create a CNAME record under your domain and get launching!
Well, it's been fun
I guess...like...we're kinda done?

Congrats! You built it!
I guess we've done it!
It doesn't have to end – I am going to gather feedback, see what people want, and then maybe continue. Let me know what you think please!
Telegram channel: https://t.me/frameworkless
Twitter: @mtimofiiv
See today's code: https://github.com/frameworkless-js/remind.ist/tree/stage/6
See today's lesson running live: https://remind.ist

frameworkless.js -> Part 6
By Mike Timofiiv
frameworkless.js -> Part 6
Check out https://frameworkless.js.org/course/6 for the whole presentation
- 1,609
 
   
   
  