Search code examples
node.jsazuresendgridazure-functions-node.js

Using SendGrid with Azure node.js programming model v4


My goal is to send email from an Azure Static Web App contact page using an Azure Function. I have managed to get that working as described below, so my question is "how can this be done better in Azure Functions?"

  • Apparently one cannot use Azure Communications Services from within an Azure Function to send email. I was able to get this to work in the local static web app (SWA) emulation, but it fails silently when running from within Azure.

  • I punted and created a SendGrid account. This offers 100 free messages a day for free, so it's a good alternative.

  • All of the reference material I can find references integration using Azure node.js programming model v3, which includes function.json configuration files. There is even a Microsoft Docs github repo issue regarding this: https://github.com/MicrosoftDocs/azure-docs/issues/119439

  • Since Azure node.js programming model v4 (hereafter just "v4") is the default and the standard, I decided I didn't want to use v3.

  • SendGrid sample code works nearly out of the box, but when you wrap it in async function for v4, you need to await the ".send" operation. If you don't, the function will actually exit and you will get a response value of "unexpected state" (using my code; the message is still sent in a local environment, but that cannot be counted upon). This is where my weakness with JS async comes into play, because I thought then/catch were the other way of dealing with promises.

Here is my working code:

const { app } = require("@azure/functions");
const mta = require("@sendgrid/mail");

app.http("sendGrid", {
  methods: ["GET", "POST"],
  authLevel: "anonymous",
  handler: async (request, context) => {
    const key = process.env.SENDGRID_API_KEY;
    var response = 'unexpected state'
    var code = 200;

    // Authenticate
    mta.setApiKey(key);

    // Timestamp for this invocation
    const timestamp = new Date(Date.now());

    // Build a message
    const content = {
      to: "user@domain", /* <== replace with your target email address */
      from: "user@domain", /* <== replace with authenticated sender address */
      subject: `SendGrid message ${timestamp}`,
      text: `SendGrid message body text ${timestamp}`,
      html: `SendGrid message body html <strong>${timestamp}</strong>`
    };

    await mta.send(content)
            .then(() => { 
                response = `message ${timestamp} sent`;
                code = 200;
            })
            .catch((thrown) => { 
                response = `message not sent ${thrown}`;
                code = 500;
            });
    
    context.log(response);

    return { status: code, body: response };
  }
});

Solution

  • To send email from an Azure Static Web App using an Azure Function of v4 you have to add frontend app to call the API endpoint. I used _src/index.html_ file to fetch the text from the API function and display it on the screen which acts like no framework .

    I used this MSDOC to add an API to Azure Static Web Apps with Azure Functions .

    Below simple HTML code is used to trigger/display the results Azure Function for sending an email using SendGrid.

    
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>API Response Display</title>
        <style>
            body {
                font-family: Arial, sans-serif;
                margin: 20px;
            }
            .response {
                margin-top: 20px;
                padding: 20px;
                border: 1px solid #ccc;
                border-radius: 5px;
            }
            .success {
                background-color: #e6ffed;
                border-color: #a3d9a5;
            }
            .failure {
                background-color: #ffe6e6;
                border-color: #d9a5a5;
            }
        </style>
    </head>
    <body>
        <h1>API Response Display</h1>
        <button id="fetchButton">Fetch API Response</button>
        <div id="responseContainer" class="response"></div>
    
        <script>
            document.getElementById('fetchButton').addEventListener('click', async () => {
                const responseContainer = document.getElementById('responseContainer');
                responseContainer.innerHTML = 'Fetching...';
    
                try {
                    const response = await fetch('/api/httpTrigger');
                    const data = await response.json();
    
                    if (response.ok) {
                        responseContainer.innerHTML = `
                            <div class="success">
                                <h2>Success</h2>
                                <pre>${JSON.stringify(data, null, 2)}</pre>
                            </div>
                        `;
                    } else {
                        responseContainer.innerHTML = `
                            <div class="failure">
                                <h2>Failure</h2>
                                <pre>${JSON.stringify(data, null, 2)}</pre>
                            </div>
                        `;
                    }
                } catch (error) {
                    responseContainer.innerHTML = `
                        <div class="failure">
                            <h2>Error</h2>
                            <pre>${error}</pre>
                        </div>
                    `;
                }
            });
        </script>
    </body>
    </html>
    
    

    Git Folder Strucure:

    enter image description here

    • Now change the git yml according to your Folder Strucure
     app_location to  "./src" 
      api_location to  "./api1"
     output_location to  "." 
    

    Refer to this SO for yml workflow configuration file in Static Web App. Deployment Status

    enter image description here

    The endpoint of the function app must have the /api prefix, since Static Web Apps matches requests made to /api. The existing Azure Functions app exposes an endpoint via the following sample example Url https:/nameofazuresataticwebapp.azurewebsites.net/api/functionname as show in the below image.

    Azure Functions app with Azure Static Output:

    enter image description here

    enter image description here

    Azure Static Output: enter image description here

    enter image description here