Sending emails from applications now don evolve significantly from how e take dey be over the years. The normal way wey be username and password authentication for Gmail SMTP wey we been dey use before no really dey market again because of some kind security matters and Google's own restriction for apps wey dey considered "less secure". The latest way now wey secure pass na to use Gmail OAuth2 authentication, with another external emailing padi like Nodemailer. I been just run am for my portfolio na why e sweet me to write like this, make another person for fit find am helpful. For this guide, you go sabi how to: Configure a Google Cloud Platform (GCP) project Enable the Gmail API Configure the OAuth Consent Screen Generate OAuth credentials Generate a refresh token Configure Nodemailer with Gmail OAuth2 Handle common errors Secure your application properly This tutorial go work for: Node.js Express Nuxt server routes Vue backend APIs Next.js API routes TypeScript projects Table of Contents Why use Gmail OAuth2? Prerequisites Creating a Google Cloud Project Enabling the Gmail API Configuring the OAuth Consent Screen Creating OAuth Credentials Installing Dependencies Generating a Refresh Token Configuring Nodemailer Sending Emails Escaping HTML and Preventing Injection Common Errors and Fixes Production Recommendations Final Thoughts Why use Gmail OAuth2? Google don deprecate the password-based SMTP authentication for many applications. OAuth2 dey offer: Better security Token-based authentication No exposed Gmail passwords Controlled access scopes Revocable access Instead make you do something like: auth : { user : " example@gmail.com " , pass : " gmail-password " } You go just do like this: auth : { type : " OAuth2 " , user , clientId , clientSecret , refreshToken } Prerequisites Before you start cook anything, make sure say the following ingredients dey ground: A Google account Node.js installed npm or yarn A backend/server environment Basic knowledge of JavaScript or TypeScript Creating a Google Cloud Project Go to the Google Cloud Console: Google Cloud Console Step 1: Create new project Click the project dropdown wey dey the top bar Click New Project Enter project name wey you want Click Create Wait for the project make e initialize. Enabling the Gmail API Once your project don create: Open the APIs Library: Google APIs Library Search for: Gmail API Click the Gmail API Click Enable If you no enable this Gmail API, the OAuth email sending go just dey chop dust. Configuring the OAuth Consent Screen This step na must before you go generate OAuth credentials. Navigate go: OAuth Consent Screen Step 1: Choose user type Choose: External Then click Create . Step 2: Fill the app information Provide: App name Support email Developer contact email Example: App Name: Portfolio Mailer Step 3: Add test users If your app still dey for testing mode: Open the Test Users section Add the Gmail account wey you wan dey use for sending the emails If you jump this step pass, you go dey jam error: unauthorized_client Creating OAuth credentials Now, na to create the OAuth credentials. Go to: Google Cloud Credentials Step 1: Create OAuth client ID Click Create Credentials Select OAuth Client ID Step 2: Select application type Choose: Web Application No go choose: Desktop App Android iOS Step 3: Add redirect URI Under the following section: Authorized redirect URIs Add: http://localhost:3000 or your preferred port. This one na must if we wan generate refresh tokens. Step 4: Save credentials After you don done, Google go provide: Client ID Client Secret Save them securely, you fit copy am or you fit download am. Installing dependencies Install the packages wey you go need: npm install nodemailer googleapis dotenv For TypeScript, also run: npm install -D tsx typescript Environment variables Create a .env file: SMTP_USER=your-email@gmail.com
GOOGLE_CLIENT_ID=your-client-id
GOOGLE_CLIENT_SECRET=your-client-secret
GOOGLE_OAUTH_REFRESH_TOKEN=your-refresh-token No go ever commit .env files go GitHub, dem suppose don sing this one for your ear enough. Generating a refresh token This na one of the most important part for this setup. Step 1: Create a script Create a script file: scripts/generate-refresh-token.ts for the root of your application. Then add the following code: import { google } from " googleapis " ; import dotenv from " dotenv " ; dotenv . config (); const oauth2Client = new google . auth . OAuth2 ( process . env . GOOGLE_CLIENT_ID , process . env . GOOGLE_CLIENT_SECRET , " http://localhost:3000 " ); const scopes = [ " https://mail.google.com/ " , ]; const url = oauth2Client . generateAuthUrl ({ access_type : " offline " , scope : scopes , prompt : " consent " , }); console . log ( " Authorization url: " , url ); Step 2: Run the script Using TypeScript, run: npx tsx scripts/generate-refresh-token.ts Or JavaScript: node scripts/generate-refresh-token.js Step 3: Open the authorization URL The terminal go vomit the Google authorization URL give you. Copy the link open am for your browser. Login with the same Gmail account as: SMTP_USER And approve the permissions wey go come up. Step 4: Copy the authorization code After the approval, Google go redirect go the url wey you set for step 3 when you dey create the OAuth credentials: http://localhost:3000/?code=... Copy the code value. Example: 4/0AX4XfWh... Step 5: Exchange code for the refresh token Update the script with the following: import { google } from " googleapis " ; import dotenv from " dotenv " ; dotenv . config (); const oauth2Client = new google . auth . OAuth2 ( process . env . GOOGLE_CLIENT_ID , process . env . GOOGLE_CLIENT_SECRET , " http://localhost:3000 " ); async function getRefreshToken () { const code = " PASTE_AUTHORIZATION_CODE_HERE " ; const { tokens } = await oauth2Client . getToken ( code ); console . log ( " Get tokens: " , tokens ); } getRefreshToken (); Step 6: Run the script again npx tsx scripts/generate-refresh-token.ts You suppose see something wey go be like this: { "access_token" : "..." , "refresh_token" : "..." , "scope" : "https://mail.google.com/" , "token_type" : "Bearer" } Copy the refresh token put inside the .env . Configuring Nodemailer Now make we configure the transporter. import nodemailer from " nodemailer " ; import { google } from " googleapis " ; const clientId = process . env . GOOGLE_CLIENT_ID ! ; const clientSecret = process . env . GOOGLE_CLIENT_SECRET ! ; const refreshToken = process . env . GOOGLE_OAUTH_REFRESH_TOKEN ! ; const user = process . env . SMTP_USER ! ; const oauth2Client = new google . auth . OAuth2 ( clientId , clientSecret , " https://developers.google.com/oauthplayground " ); oauth2Client . setCredentials ({ refresh_token : refreshToken , }); async function createTransporter () { const accessToken = await oauth2Client . getAccessToken (); return nodemailer . createTransport ({ service : " gmail " , auth : { type : " OAuth2 " , user , clientId , clientSecret , refreshToken , accessToken : accessToken . token || undefined , }, }); } Sending emails Example: const transporter = await createTransporter (); await transporter . sendMail ({ from : "Portfolio Contact" < ${ user } > , replyTo : " visitor@example.com " , to : user , subject : " New Portfolio Message " , text : `
Name: John Doe
Email: visitor@example.com
Message:
Hello there!
, html :
New Portfolio Message
<p><strong>Name:</strong> John Doe</p>
<p>
<strong>Email:</strong>
visitor@example.com
</p>
<p>Hello there!</p>
</div>
` , }); Important: Use replyTo instead of from This one na one common mistake wey fit happen. No go do like this: from : " visitor@example.com " Gmail fit reject am, e dey get choko sometimes. Instead, do am like this: from : `"Portfolio Contact" < ${ user } >` , replyTo : visitorEmail , This one dey sweet Gmail belle, e go still allow make you fit send direct replies give the visitor. Escaping HTML and preventing injection No go ever inject raw user input put inside the HTML. This no safe at all and proper wahala fit brew if anyhow person go jam am: html : `<p> ${ message } </p>` Create an escapeHtml helper function, this way dey safe enough: function escapeHtml ( unsafe : string ) { return unsafe . replace ( /&/g , " & " ) . replace ( /</g , " < " ) . replace ( />/g , " > " ) . replace ( /"/g , " " " ) . replace ( /'/g , " ' " ); } or install the package, run: npm install --save-dev escape-html @types/escape-html Then use am like this: const safeMessage = escapeHtml ( message ); Then pass am: html : `<p> ${ safeMessage } </p>` Recommended security improvements Validate email inputs const emailRegex = /^ [^\s @ ] +@ [^\s @ ] + \.[^\s @ ] +$/ ; if ( ! emailRegex . test ( email )) { throw new Error ( " Invalid email " ); } Limit input length if ( message . length > 5000 ) { throw new Error ( " Message too long " ); } Rate limit requests Mount security for your form make you secure am from spam. Recommended: Cloudflare Turnstile reCAPTCHA IP rate limiting Common errors and fixes unauthorized_client Cause: OAuth consent no dey configured correctly Gmail account no dey added as test user Wrong OAuth client type Fix: Add yourself as a test user Use Web Application OAuth client redirect_uri_mismatch Cause: Missing redirect URI Fix: Add the local server with your desired port: http://localhost:3000 to: Authorized redirect URIs invalid_grant Cause: Expired or revoked refresh token Fix: Generate a new refresh token Username and password not accepted Cause: Using password auth instead of the OAuth2 Fix: Use OAuth2 credentials Production recommendations For small forms like the portfolio/contact forms, the Gmail OAuth2 works well. However, for scalable production apps, consider dedicated email providers like: Resend Mailgun Brevo Advantages: Better deliverability Easier setup Analytics Fewer Gmail restrictions Full code implementation My full code implementation inside my Nuxt server component for my portfolio import nodemailer from " nodemailer " ; import escapeHtml from " escape-html " ; export default defineEventHandler ( async ( event ) => { const body = await readBody ( event ); if ( ! body ) { throw createError ({ statusCode : 400 , statusMessage : " Invalid request payload. Body is required. " , }); } const { name , email , subject , message , } = body ; // Validation if ( ! name || typeof name !== " string " || ! name . trim ()) { throw createError ({ statusCode : 400 , statusMessage : " Name is required and must be a valid text. " , }); } if ( ! email || typeof email !== " string " || ! email . trim ()) { throw createError ({ statusCode : 400 , statusMessage : " Email address is required. " , }); } const emailRegex = /^ [^\s @ ] +@ [^\s @ ] + \.[^\s @ ] +$/ ; if ( ! emailRegex . test ( email . trim ())) { throw createError ({ statusCode : 400 , statusMessage : " Please provide a valid email address. " , }); } if ( ! message || typeof message !== " string " || ! message . trim ()) { throw createError ({ statusCode : 400 , statusMessage : " Message is required and must be a valid text. " , }); } if ( message . length > 5000 ) { throw createError ({ statusCode : 400 , statusMessage : " Message too long " , }); } const trimmedName = escapeHtml ( name . trim ()); const trimmedEmail = escapeHtml ( email . trim ()); const trimmedSubject = escapeHtml ( subject . trim ()); const trimmedMessage = escapeHtml ( message . trim ()); // Dispatching const user = process . env . SMTP_USER ; const receiver = process . env . CONTACT_EMAIL_RECEIVER ?? user ; const clientId = process . env . GOOGLE_CLIENT_ID ; const clientSecret = process . env . GOOGLE_CLIENT_SECRET ; const refreshToken = process . env . GOOGLE_OAUTH_REFRESH_TOKEN ; if ( user && clientId && clientSecret && refreshToken ) { try { const transporter = nodemailer . createTransport ({ service : " gmail " , auth : { type : " OAuth2 " , user : user , clientId : clientId , clientSecret : clientSecret , refreshToken : refreshToken , } }) await transporter . sendMail ({ from : `"Portfolio Contact" < ${ user } >` , replyTo : trimmedEmail , to : receiver , subject : ` ${ trimmedSubject ?? `Portfolio Message from ${ trimmedName } ` } ` , text : `Name: ${ trimmedName } \nEmail: ${ trimmedEmail } \n\nMessage:\n ${ trimmedMessage } ` , html : `
<div style="font-family: sans-serif; padding: 20px; color: #333; max-width: 600px; border: 1px solid #e2e8f0; border-radius: 12px;">
<h2 style="color: #235347; border-bottom: 2px solid #235347; padding-bottom: 8px;">New Contact Form Message</h2>
<p><strong>Name:</strong> ${ trimmedName } </p>
<p><strong>Email:</strong> <a href="mailto: ${ trimmedEmail } "> ${ trimmedEmail } </a></p>
<p><strong>Message:</strong></p>
<blockquote style="background: #f7fafc; padding: 15px; border-left: 4px solid #235347; margin: 0; white-space: pre-wrap;"> ${ trimmedMessage } </blockquote>
<hr style="border: 0; border-top: 1px solid #e2e8f0; margin: 20px 0;" />
<p style="font-size: 0.875rem; color: #718096;">Sent from your Portfolio contact form.</p>
</div>
` , }); return { success : true , message : " Message sent successfully via SMTP! " , }; } catch ( error : any ) { console . error ( " SMTP sending error: " , error ); throw createError ({ statusCode : 500 , statusMessage : `Failed to send email. Error: ${ error . message || " SMTP Configuration Issue " } ` , }); } } }) Final thoughts Setting up Gmail OAuth2 with Nodemailer fit be like say na heavy or too much work because say the following dey involved: Google Cloud configuration OAuth flows API permissions Token generation But once you don configure am properly, e go provide a secure and production-ready email solution wey no need make you expose your Gmail password. The key steps no pass: Enable Gmail API Configure OAuth consent Create OAuth credentials Generate a refresh token Configure Nodemailer correctly Use replyTo instead of arbitrary from addresses Escape user input to prevent injection Once all these things don gather stand well, your application fit dey use Gmail and OAuth2 authentication dey send emails securely, you mind sef go dey at rest. I hope say you go find am usefully helpful. Cheers🥂!


