DEV Community

loading...
Cover image for Sending Email with Netlify Functions

Sending Email with Netlify Functions

Jenna Pederson
sr developer advocate @awscloud / board @minnestar / she/her / opinions are mine and only mine
Originally published at jennapederson.com ・7 min read

Functions, lambdas, serverless. Maybe you've heard of these. Maybe you haven't. These words are being used to describe a different way of connecting your frontend code to backend functionality. Without having to run a full-blown backend app server. Or even manage infrastructure. We're empowering front-end developers to be able to accomplish backend tasks like sending an email or making a call to another third-party API by creating and deploying serverless functions. Netlify abstracts away even more complexity for us and uses AWS Lambda behind the scenes. Our Netlify functions can live alongside our site and make use of the same deployment process, making it easier for us to manage, support, and operate.

A prime use case for a serverless function is to send a text message or an email. Here we'll create a contact form that sends you an email using a serverless function. We'll use our starter-for-nuxt-markdown-blog from here and work through the following steps:

  • Install the Netlify CLI
  • Update netlify.toml
  • Create the function
  • Setup a mailer service
  • Send the email
  • Try it out locally
  • Build the contact page to call the function
  • Configure environment variables in Netlify
  • Try it out live

And end up withΒ this.

Or if you're impatient, grab the final version from the GitHub repoΒ here.

Install the Netlify CLI

First, we'll install the Netlify CLI. This will allow us to run our functions locally, from the command line alongside our app.

yarn global add netlify-cli

Update netlify.toml

Update your netlify.toml file by adding functions = "functions" to the [build] section. If you don't already have a [dev] section, also add that with command = "yarn dev".

Now you can start up your app with netlify dev where it'll run the same yarn dev command as before, but it'll also serve your functions in the functions directory and run a proxy server on port 8888 so both your site and functions are on the same port.

But now we need a function!

Create the function

Run this to create a stub hello world function:

netlify functions:create send-contact-email

Select the [hello-world] template. This will create functions/send-contact-email/send-contact-email.js with a basic hello world template.

To test this right now from the command line, run:

netlify functions:invoke send-contact-email --no-identity

Or point your browser at http://localhost:8888/.netlify/functions/send-contact-email

You should see the following response:

{"message":"Hello World"}

Setup a mailer service

There are many good players in this space. I love Postmark (because of their UI, templates, and reliability) and Sendgrid, but Mailgun also works and is cheap for our purposes (aka has a free tier), so we'll start there.

First, head over to Mailgun and create your account.

Now we'll need to wire up Mailgun to send the email to us when that function is triggered.

We'll use dotenv to access environment variables in our local environment and configure Netlify to use the right values in production.

Install the dotenv and mailgun-js dependencies:

yarn add dotenv mailgun-js

Create a file in the project root called .env with the following, replacing these values with those in your own Mailgun account:

MAILGUN_API_KEY=key-SOME_MAILGUN_KEY
MAILGUN_DOMAIN=sandboxSOME_MAILGUN_SANDBOX_DOMAIN.mailgun.org
MAILGUN_URL=https://api.mailgun.net/v3/sandboxSOME_MAILGUN_SANDBOX_DOMAIN.mailgun.org

FROM_EMAIL_ADDRESS=YOUR_EMAIL_ADDRESS
CONTACT_TO_EMAIL_ADDRESS=YOUR_EMAIL_ADDRESS
Enter fullscreen mode Exit fullscreen mode

DO NOT COMMIT this file to version control! Make sure you've added a rule to .gitignore. This file is meant to contain sensitive information and should ONLY live on your computer.

Send the email

Then replace your send-contact-email.js file with this:

    require('dotenv').config()
    const { MAILGUN_API_KEY, MAILGUN_DOMAIN, MAILGUN_URL, FROM_EMAIL_ADDRESS, CONTACT_TO_EMAIL_ADDRESS } = process.env
    const mailgun = require('mailgun-js')({ apiKey: MAILGUN_API_KEY, domain: MAILGUN_DOMAIN, url: MAILGUN_URL })

    exports.handler = async (event) => {
      if (event.httpMethod !== 'POST') {
        return { statusCode: 405, body: 'Method Not Allowed', headers: { 'Allow': 'POST' } }
      }

      const data = JSON.parse(event.body)
      if (!data.message || !data.contactName || !data.contactEmail) {
        return { statusCode: 422, body: 'Name, email, and message are required.' }
      }

      const mailgunData = {
        from: FROM_EMAIL_ADDRESS,
        to: CONTACT_TO_EMAIL_ADDRESS,
        'h:Reply-To': data.contactEmail,
        subject: `New contact from $`,
        text: `Name: $\nEmail: $\nMessage: $


      return mailgun.messages().send(mailgunData).then(() => ({
        statusCode: 200,
        body: "Your message was sent successfully! We'll be in touch."
      })).catch(error => ({
        statusCode: 422,
        body: `Error: $
      ))
    }
Enter fullscreen mode Exit fullscreen mode

A few things to note:

The require('dotenv').config() line loads the dotenv environment so that we can access environment variables via process.env as you can see on lines 1 and 2.

On line 3, we're initializing the Mailgun client with our environment variables.

On lines 6-13, we're doing some additional validation to make sure it's the right HTTP method (post) and that the function has been passed the correct data.

Lines 15-21 prepare the data we need to pass to Mailgun such as the from/to email address, a reply-to email, a subject line, and the actual message (text for now).

Lines 23-29 sends the email, handles a successful response from Mailgun or an error response from Mailgun.

Why are we using a server-side function rather than shoving this code right into our front-end code?

We use a server-side function for security purposes. Our client-side code is readable by anyone and we don't want to expose our Mailgun api key to the world. So we push it to a server.

Try it out locally

If you haven't restarted since creating the .env file or changing those values, you'll need to restart with netlify dev.

You won't be able to hit it from a browser anymore because we locked it down to POSTs, but that's always a good test case to try!

You can use the command line:

netlify functions:invoke send-contact-email --no-identity

This will produce another error case: Name, email, and message are required.

Add the required parameters with the --payload option:

netlify functions:invoke send-contact-email --no-identity --payload '{"contactEmail" : "jenna@example.com", "contactName" : "Jenna", "message" : "hello world from a function!"}'
Enter fullscreen mode Exit fullscreen mode

You should see Your message was sent successfully! We'll be in touch. at the command line and an email in your inbox.

Build the contact page to call the function

Now we need to make a contact form that triggers the function so that visitors to our site can contact us.

We'll use Axios to make our call to the function, so let's add that package:

yarn add @nuxtjs/axios

And configure it in nuxt.config.js:

    ...
    modules: [
        ...
      '@nuxtjs/axios'
    ],
    axios: {
      baseURL: '/'
    },
    ...
Enter fullscreen mode Exit fullscreen mode

Then, we'll create a new page pages/contact.vue with a form like this:

    <template>
      <div class="container">
        <h1 class="title">
          Contact
        </h1>

        <article v-for="msg in messages"
          :key="msg.text"
          class="message"
          :class="msg.type === 'success' ? 'is-success' : 'is-danger'">
          <div class="message-body">
            {  }
          </div>
        </article>

        <section class="contact-form">
          <div class="field">
            <label class="label">Name</label>
            <div class="control">
              <input v-model="contactName" class="input" type="text">
            </div>
          </div>

          <div class="field">
            <label class="label">Email</label>
            <div class="control">
              <input v-model="contactEmail" class="input" type="email">
            </div>
          </div>

          <div class="field">
            <label class="label">Your Message</label>
            <div class="control">
              <textarea v-model="contactMessage" class="textarea" />
            </div>
          </div>

          <div class="field is-grouped">
            <div class="control">
              <button class="button is-link" @click="sendMessage">
                Send Message
              </button>
            </div>
            <div class="control">
              <button class="button is-text" @click="cancelMessage">
                Cancel
              </button>
            </div>
          </div>
        </section>
      </div>
    </template>

    <script>
    export default {
      data () {
        return {
          messages: [],
          contactName: '',
          contactEmail: '',
          contactMessage: ''
        }
      },
      methods: {
        sendMessage () {
          this.messages = []
          this.triggerSendMessageFunction()
        },
        cancelMessage () 

        ,
        resetForm () {
          this.messages = []
          this.contactName = ''
          this.contactEmail = ''
          this.contactMessage = ''
        },
        async triggerSendMessageFunction () {
          try {
            const response = await this.$axios.$post('/.netlify/functions/send-contact-email', {
              contactName: this.contactName,
              contactEmail: this.contactEmail,
              message: this.contactMessage
            })
            this.resetForm()
            this.messages.push({ type: 'success', text: response })
          } catch (error) {
            this.messages.push({ type: 'error', text: error.response.data })
          }
        }
      }
    }

    </script>
Enter fullscreen mode Exit fullscreen mode

Now, with netlify dev running, point your browser at localhost:8888/contact. Be sure to use the proxy port 8888 for the site as that is the same port that our functions are running on.

Let's run through what's in that file.

The template is typical Vue stuff where we show error and success messages on lines 7-14 and for each input, we bind to a data property using v-model. We've also got a typical @click handler on each button.

In the script block, we set up our data on lines 56-63 with an array for error/success messages and the form data we want to submit to our function.

The most interesting part here is the async triggerSendMessageFunction on lines 78-90. Here we're using Axios to make our POST call to our new function, passing it our form data. As we learned earlier, our function will live in our functions directory inside Netlify's .netlify directory. Based on our response, success or error, we also add to the messages array.

Give it a whirl and check to see that you've got an email in your inbox! Or that the error handling works as you would expect.

Configure environment variables in Netlify

A couple more steps and this will be live! Assuming you've already got your site deploying to Netlify on push to your master branch, you'll need to set up the environment variables that we used in our .env file.

In your Netlify account, navigate to the Build & Deploy - Environment page for your site.

Add these keys and the production values you want to use. Ideally, you would use the sandbox keys for your local development and testing purposes and a real Mailgun domain for production, but to test this out, you can use the sandbox version here as well.

MAILGUN_API_KEY=
MAILGUN_DOMAIN=
MAILGUN_URL=

FROM_EMAIL_ADDRESS=
CONTACT_TO_EMAIL_ADDRESS=
Enter fullscreen mode Exit fullscreen mode

After adding these environment variables, you'll need to trigger a redeploy of your functions so that they are picked up. You can do this on Netlify's Deploys tab in the Trigger Deploy dropdown.

Try it out live

Give it a whirl!

Hopefully, this gets you started with Netlify functions! You can grab this version of the code from here.

Ready to get started with this as your starter on Netlify right now? You can do that too!

Deploy to Netlify

Discussion (21)

Collapse
marieqg profile image
marieqg

Hello Jenna,
I got stuck at the first step :(
when I do : "netlify functions:invoke send-contact-email --no-identity"
I get the following error :

$ netlify functions:invoke send-contact-email --no-identity
ran into an error invoking your function
{ FetchError: request to http://localhost:8888/.netlify/functions/send-contact-email failed, reason: connect ECONNREFUSED 127.0.0.1:8888
at ClientRequest.<anonymous> (/usr/local/lib/node_modules/netlify-cli/node_modules/node-fetch/lib/index.js:1455:11)
at ClientRequest.emit (events.js:198:13)
at Socket.socketErrorListener (_http_client.js:392:9)
at Socket.emit (events.js:198:13)
at emitErrorNT (internal/streams/destroy.js:91:8)
at emitErrorAndCloseNT (internal/streams/destroy.js:59:3)
at process._tickCallback (internal/process/next_tick.js:63:19)
message:
'request to http://localhost:8888/.netlify/functions/send-contact-email failed, reason: connect ECONNREFUSED 127.0.0.1:8888',
type: 'system',
errno: 'ECONNREFUSED',
code: 'ECONNREFUSED' }

Any tips on how to get ride of this error?

Collapse
jennapederson profile image
Jenna Pederson Author

Hi Marie -

The ECONNREFUSED error indicates that it can't connect to where the function is hosted (in this case localhost:8888). Make sure that you have netlify dev running in another command line window so that the functions are accessible.

Hope that helps!

Collapse
quantuminformation profile image
Nikos

When I run netlify dev I get


  Your application is ready~! πŸš€

  ➑ Port 5000 is taken; using 58057 instead

  - Local:      http://localhost:58057

────────────────── LOGS ──────────────────

.......

and when running netlify functions:invoke send-contact-email --no-identity



ran into an error invoking your function
FetchError: request to http://localhost:8888/.netlify/functions/send-contact-email failed, reason: connect ECONNREFUSED 127.0.0.1:8888
    at ClientRequest.<anonymous> (/Users/nikos/.nvm/versions/node/v12.13.1/lib/node_modules/netlify-cli/node_modules/node-fetch/lib/index.js:1455:11)
    at ClientRequest.emit (events.js:210:5)
    at Socket.socketErrorListener (_http_client.js:406:9)
    at Socket.emit (events.js:210:5)
    at emitErrorNT (internal/streams/destroy.js:92:8)
    at emitErrorAndCloseNT (internal/streams/destroy.js:60:3)
    at processTicksAndRejections (internal/process/task_queues.js:80:21) {
  message: 'request to http://localhost:8888/.netlify/functions/send-contact-email failed, reason: connect ECONNREFUSED 127.0.0.1:8888',
  type: 'system',
  errno: 'ECONNREFUSED',
  code: 'ECONNREFUSED'```

Thread Thread
stevealee profile image
SteveALee • Edited

Did you resolve this? I'm getting exactly the same issue trying to call a simple function using netlify dev. Have spent ages on it.
I used 'netlify.toml' to define functions="functions/"

Is this possibly a WIndows issue?

Thread Thread
jennapederson profile image
Jenna Pederson Author

Hi Steve - Did you get this ironed out? What's the error message you're seeing?

If it's the same as the one that Marie ran into

reason: connect ECONNREFUSED 127.0.0.1:8888

then make sure that you've started up your app with netlify dev first.

If it is started, check to see what port is being used. You should see a message that the server has started. There are 3 that are started - the app, the lambda server where your functions run, and a proxy server (which is a proxy to both of those), so be sure to look for the one that is for the proxy server. The message looks like this:

Proxy server

If that port is NOT 8888, then the netlify functions:invoke call will fail since it's looking for functions on port 8888.

It doesn't look like there is an option in the command line to specify a port here, so there are a couple of options: 1) Figure out what is using port 8888 and stop it so that this will work or 2) Add the following to your [dev] block in your netlify.toml file:

port = 4444 # Port that the proxy server will listen on

This will force it to use the port you tell it to use and netlify functions:invoke will also use that port.

Make sure to change 4444 to an unused port on your system.

Thread Thread
stevealee profile image
SteveALee

Actually, it seems the lastest netlify-cli is broken. After a lot of head scratching I found this issue and intalled an old version and the problem went away

github.com/netlify/cli/issues/659

Thanks for answering so quickly

Thread Thread
jennapederson profile image
Jenna Pederson Author

Oh bummer! Glad you got it sorted though.

Collapse
marieqg profile image
marieqg

Thanks for your quick reply. I had the netlify dev running in another command line, that's why I don't understand ^

Collapse
thetechnaddict profile image
thetechnaddict

My error is: Function invocation failed: [object Object] - without the payload and with it... ummm

Collapse
thetechnaddict profile image
thetechnaddict • Edited

agh, yes - install the dependencies, well done genius - now I'm forbidden, but definitely getting there!!

I'm loving the tutorial - it is super helpful, Jenna, thanks

Collapse
jennapederson profile image
Jenna Pederson Author

Thanks! Glad you got it ironed out.

Thread Thread
thetechnaddict profile image
thetechnaddict

Sadly not yet - I'm now forbidden - I can connect and send using curl but the js version is rejected.

Here is the function paired down to basics with hard-coded everything just to get mailgun working

const mailgun = require('mailgun-js');

exports.handler = async (event) =>
{
const mg = mailgun({
apiKey: "key-exampleofverylongsecretkey",
domain: "mg.example.com",
url: "api.eu.mailgun.net/v3/mg.example.com"
});

const data = {
from: 'Name mailgun@mg.example.com',
to: 'bored@otherexample.com',
subject: 'Worldish',
text: 'Hello W',
html: 'HTML'
};

return mg.messages().send(data).then(() => ({
statusCode: 200,
body: "Your message was sent successfully! We'll be in touch."
})).catch(error => ({
statusCode: 422,
body: Error: ${error}
}));
}




ps - I've tried it with 'key-exampleofsecretcode' and 'exampleofsecretcode' and with and without the url and with the url with and without /messages - same result for all
Thread Thread
thetechnaddict profile image
thetechnaddict

I went back to testing with the sandbox and the email sent - but ended up in spam. -which could be reasonable since it claims to be from one domain and has been sent by another. - I guess I just have to test it on the server

Thread Thread
thetechnaddict profile image
thetechnaddict

If I hardcode sandbox secrets into the file it sends happily from the localhost. If I then switch to using dotenv it fails suggesting that require('dotenv').config() isn't loading up process.env

Thread Thread
thetechnaddict profile image
thetechnaddict

Reinstalling dotenv has sorted the problem

Collapse
timtrautman profile image
Tim Trautman

Great post!

I've loved playing around with Netlify functions -- it makes working with AWS lambda so much more approachable (for me, at least.) And it's neat to see how quickly it lets you build out small apps with backend functionality (like email!) without having to worry about the operations of standing up a backend server.

But, on the flip side, have you run into any difficulties with them? I've found debugging to be a bit more difficult and time-consuming, but that might subside as I become more familiar with them and as netlify dev gets a bit more mature.

Collapse
jennapederson profile image
Jenna Pederson Author

Thanks for the feedback, Tim!

Such a great question too. I have one live project (other than this playground) using a few functions and with not much traffic, so I haven't had a lot of experience with where it falls down. One thing I found (for that particular implementation) was that since there was not much traffic hitting those functions, they would occasionally fail/timeout as they were spinning up. Another use case I've heard they are not well suited for is long-running processes because of this low timeout value.

I started digging into this because I wanted to learn more about that specifically so that I can make better decisions on whether to go down this route for future projects. Stay tuned for a future post on!

And I'm all ears on where you find it tricky or where it doesn't work well, beyond the debugging process.

Collapse
jeffwscott profile image
Jeff Scott

netlify functions:invoke send-contact-email --no-identity --payload '{"contactEmail" : "jenna@example.com", "contactName" : "Jenna", "message" : "hello world from a function!"}'

this produces this error:

Error: Sandbox subdomains are for test purposes only. Please add your own domain or add the address to authorized recipients in Account Settings.

Collapse
jennapederson profile image
Jenna Pederson Author

Hey Jeff -

Based on that error, you'll either have to add the email address you're using in the CONTACT_TO_EMAIL_ADDRESS env var to the authorized recipients in your Mailgun Account Settings (see more about that here) or move off the sandbox domain to your very own custom domain.

Hope that helps!

Collapse
quantuminformation profile image
Nikos

Is yarn really necessary in 2019?

Collapse
thetechnaddict profile image
thetechnaddict

I seem to be getting away with using npm - but I'm coming late to the party :-)