A minimalistic Express mail microservice using SendGrid API and JWT authentication
Tags: express, microservices, backend, webdev
Published: 2023-10-23 14:17:00

This is a simple and lean email microservice used in a bigger open source project I am currently publishing. I invite you to follow me if you are interested in the authentication gateway that uses this service... to be found on DEV.to some time in the near future.

You can find the repository for the microservice here:

https://github.com/p-michael-b/ExpressMailService

I am open for all sorts of input on this, I am a self-taught developer and here to learn...

With this out of the way, let's get right to it:

We are using the ExpressJS framework with a few standard libraries for the build, because we don't want to reinvent the wheel. 

The SendGrid API offers a few advantages in comparison to Nodemailer, deliverability being on top of the list in my opinion. To make this service work, you will have to create a SendGrid account accordingly, if you don't have one already. 

For authenticating and authorizing the service requests from the caller, we are using JSON Web Tokens (JWT). 

The first few lines of code are mostly the imports for the above mentioned:

```javascript
// Load environment variables from a .env file
require('dotenv').config();

// Import the Express.js framework
const express = require('express');

// Import morgan middleware for request logging (for debugging and monitoring)
const morgan = require('morgan');

// Import cors middleware for enabling Cross-Origin Resource Sharing
const cors = require('cors');

// Import helmet middleware for enhancing security by setting various HTTP headers
const helmet = require('helmet');

// Define the port for your Express app, allowing it to be set through an environment variable
const PORT = process.env.SERVER_PORT || 5002;

// Creating an Express application instance.
const app = express();

// Creating an HTTP server instance to serve our Express app.
const server = require('http').Server(app);

// Importing the SendGrid library for sending email notifications.
const sendgrid = require('@sendgrid/mail');


// Importing the JSON Web Token (JWT) library for authentication and authorization.
const jwt = require('jsonwebtoken');
```
Next is a simple check if all of the environment variables have been set. To make it work you will have to specify your SendGrid API key, your JWT secret (same as in caller) and the desired email sending address in the .env file of the codebase.

Next are configurations of the libraries we imported and a custom log we use to get a nice log every time the service is being used. This can be removed in a production environment if necessary of course.

```javascript
//Importing the environment variables and making sure they are all available
const requiredEnvVars = ['SENDGRID_API_KEY', 'JWT_SECRET', 'SENDER', 'SERVER_PORT'];
for (const envVar of requiredEnvVars) {
    if (!process.env[envVar]) {
        console.error(`Missing required environment variable: ${envVar}`);
        process.exit(1);
    }
}

// Parsing incoming JSON data in requests using Express middleware.
app.use(express.json());

// Configuring Cross-Origin Resource Sharing (CORS) middleware with credentials and origin options.
// credentials flag allows the client to send the session info in the header
// origin flag allows the server to reflect (enable) the requested origin in the CORS response
// this is a low security origin flag, should be changed in production
app.use(cors({credentials: true, origin: true}))

// Custom Morgan token to log the request body as a JSON string.
morgan.token('body', (req) => JSON.stringify(req.body));

// Setting up Morgan middleware to log Mail Service requests.
app.use(morgan('\n********** MAIL SERVICE REQUEST **********\n' +
    'Date       :date[iso]\n' +
    'Request    :method :url\n' +
    'Status     :status\n' +
    'Response   :response-time ms\n' +
    'Remote IP  :remote-addr\n' +
    'HTTP ver.  :http-version\n' +
    'Referrer   :referrer\n' +
    'User Agent :user-agent\n' +
    '********** END REQUEST **********\n\n'));

// Enhancing security by applying Helmet middleware for HTTP header protection.
app.use(helmet());

// Setting up SendGrid API key
sendgrid.setApiKey(process.env.SENDGRID_API_KEY);
```

As mentioned earlier, the authentication and authorization of the microservices are handled via JWT token. We implement a Middleware for this explicit purpose.

```javascript
// Middleware to verify JWT token
const verifyToken = (req, res, next) => {
    const authHeader = req.headers.authorization;
    const token = authHeader && authHeader.split(' ')[1];
    if (!token) {
        return res.status(401).json({ success: false, error: 'No token sent' });
    }
    try {
        jwt.verify(token, process.env.JWT_SECRET, function (error, decoded) {
            if (error) {
                return res.status(401).json({ success: false, error: 'Invalid Token' });
            } else {
                req.user = decoded;
                next();
            }
        });
    } catch (error) {
        return res.status(500).json({ success: false, error: 'Error verifying token' });
    }
};
```
The next code block in this article is not part of the microservice, but shows how the caller of the microservice could look like. Obviously there will be alot of code between importing the JWT library and the sendMail function in the real caller. 

Also the service that calls the microservices (In my project's case the authentication gateway) implements JWT as well and needs to have the same secret as the mail microservice in its .env for this setup to work. This is a rather straightforward way to secure the backend communication. 

Errors are being communicated from the microservice through the gateway, all the way to the frontend in a standardized way in this setup.

```javascript
//importing JSON Web Token library
const jwt = require('jsonwebtoken');

const sendMail = async (mailRecipient, mailSubject, mailText) => {
    const jwtToken = jwt.sign({}, process.env.JWT_SECRET, {expiresIn: '1h'});
    try {
        const response = await fetch(api_url, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${jwt_token}` // pass token to mail service
            },
            body: JSON.stringify({mailRecipient, mailSubject,mailText})
        })
        return response;
    } catch (error) {
        // Handle network errors or other exceptions
        console.error('Error:', error.message);
    }
}
```

Next, we implement a health route, this is standard procedure to check if the service is running okay.

```javascript
// Health Route to check if service is running
app.get('/', (req, res) => {
    return res.status(200).json({
        success: true,
        message: 'The Mail Service',
    });
})
```

Finally, the actual mail service using the SendGrid API, followed by starting the service.
```javascript
// Endpoint to send email
app.post('/sendmail', verifyToken, async (req, res) => {
    try {
        const {mailRecipient, mailSubject, mailText} = req.body;
        const msg = {
            to: mailRecipient,
            from: process.env.SENDER,
            subject: mailSubject,
            text: mailText,
        };
        await sendgrid.send(msg)
        return res.status(200).json({
            success: true,
            message: 'Email sent',
        });
    } catch (error) {
        console.error('Error sending email:', error);
        res.status(500).json({success: false, error: 'Error sending email'});
    }
});

// Starting the server and listening on the specified port, logging the listening status.
server.listen(PORT, () => console.log(`server listening on port ${PORT}`));
```

Hope this article can help you implement a similar microservice for your project. Feel free to reach out with any questions regarding this article.

Best wishes

Mat Kwa