To create serverless contact forms, you need a service like Google Cloud Functions and its powerful cloud functions. In this tutorial I will show you how to create a contact form with Firebase and React. Our contact form will send us notification emails and will write an entry in Firebase Database, so we won't delete or lose messages.
Setting up Firebase
First, you will need a Google Account and sign in into the Firebase console admin panel. Next step is to create/add a project from which you will have access to a Hosting, Realtime database, Cloud functions, etc. We can find here the list of all the Firebase products. Finally, we need to initialize Firebase in our project. You have two options:
-
If we need to choose Firebase services, like functions, hosting and database, execute in the console the command (this should be our case):
firebase init
After that follow the instruction and check the services we want to use like functions, database, hosting, etc.
-
If we will use only functions just type in the console:
firebase init functions
We can find a detailed guide in the official docs.
Our contact form will send us notification emails and will write an entry in our Firebase Database.
Start writing code
Back-end
We will start with the back-end part. Firebase uses NodeJS server and to write code, you need to navigate to functions
folder and open index.js
. We can find the folder after functions initialization mentioned above.
The packages we need to continue are firebase-functions
, nodemailer
, cors
. First one is to initiate functions, the second one is to help send emails and the last one is to enable Cross-origin (CORS) requests. We will use the last one only if we will use custom domain.
As an email service we will use Gmail. It's the easiest to set and we already have an account for firebase. To protect our email and password, we will take advantage of Environment configuration, we don't want to expose our credentials. Open the terminal in the root folder of the project and type:
firebase functions:config:set gmail.email="<user@gmail.com>" gmail.password="<pass>"
This will create an object:
{
"gmail": {
"email": "user@gmail.com",
"password": "pass"
}
}
And we can access it like this:
const gmailPassword = functions.config().gmail.password;
To create Firebase function, we need to choose a trigger. In our case we will use HTTP trigger, but there are plenty of others the list is here.
exports.submit = functions.https.onRequest((req, res) => {
cors(req, res, () => {
// write code
});
});
With this code snippet above we have a function called submit
and is triggering from GET
, POST
, PUT
, DELETE
, and OPTIONS
request. Firebase will create a special URL we can see in the Functions page.
Our working barebone function will look something like:
const functions = require('firebase-functions');
const nodemailer = require('nodemailer');
const cors = require('cors')({ origin: true });
const gmailEmail = functions.config().gmail.email;
const gmailPassword = functions.config().gmail.password;
const mailTransport = nodemailer.createTransport({
service: 'gmail',
auth: {
user: gmailEmail,
pass: gmailPassword
}
});
exports.submit = functions.https.onRequest((req, res) => {
cors(req, res, () => {
if (req.method !== 'POST') {
return;
}
const mailOptions = {
from: req.body.email,
replyTo: req.body.email,
to: gmailEmail,
subject: `from my website ${req.body.email}`,
text: req.body.message,
html: `<p>${req.body.message}`
};
mailTransport.sendMail(mailOptions);
res.status(200).end();
// or you can pass data to indicate success.
// res.status(200).send({isEmailSend: true});
});
});
What we did is to send to ourselves email with data from a POST
request. From here we can extend our function to test if data typed by the user is correct or attempt to breach security or whatever we want.
Thanks to Patrick Tobias and Vips to point out we need to terminate our http functions. In the example above we use .end()
, but we can pass data with .send()
. For example, we can use variable isEmailSend
to show and hide loading indicator.
Now we need to write the POST
request for the client side.
Front-end
Let's build a simple form with email and message fields:
<form onSubmit="{handleSubmit}">
<label>
e-mail:
<br />
<input type="email" name="email" value="{email}" onChange="{handleEmailChange}" />
</label>
<br />
<label>
Message
<br />
<textarea type="text" name="message" value="{message}" onChange="{handleMessageChange}" />
</label>
<br />
<input type="submit" value="submit" />
</form>
Then we need to initialize firebase app in the componentDidMount()
or in useEffect(() => {},[])
.
Install the Firebase package:
npm install firebase --save
And import it:
import firebase from 'firebase/app';
We will create a config object and the data can be see from our Firebase admin panel:
const config = {
apiKey: '****************',
databaseURL: 'https://ourprojectname.firebaseio.com'
};
Then we initialize the app:
if (!firebase) {
setFirebaseApp(firebase.initializeApp(config));
}
Pro Tip
At the time of writing this blog post Firebase npm package is version 5.8.3. The package is referring to the global object window
which is a problem. The problem is that if we are using an environment that has no window
object for example site generator like Gatsby
or whatever with NodeJS
we will get an error :
window.self is not defined.
If you bumped to this error, there is a simple workaround. Don't import firebase from 'firebase/app';
. And initialize the app like that:
if (!firebaseApp && window !== undefined) {
const firebase = require('firebase/app');
setFirebaseApp(firebase.initializeApp(config));
}
END of Pro Tip
Don’t forget to check if the app has only one instance.
We will focus on the handleSubmit
method which will fire when we submit the contact form.
First, we will create an object which will have the email and message and go to the back-end, something like that:
const data = { email, message };
Remember our back-end function? It takes request
and response
arguments. We can get the data
object via request.body
or in our case req.body
, check the function code above for reference.
Second, we will use Axios
lib for the Ajax request, but you are free to use something else.
npm i axios --save
Axios
POST
request has two arguments, first is the route and second is the data
object. The route is the Firebase function URL mentioned above and the data is our object we created earlier.
Our barebone method will look something like:
const handleSubmit = event => {
event.preventDefault();
const data = { email, message, time: getTime() };
Axios.post('https://us-central1-ourprojectname.cloudfunctions.net/submit', data).catch(error => {
console.log(error);
});
};
It's fine if we stop here, but we want to not only send email to ourselves, but to add an entry to our Firebase Database.
To do that, we will chain the Ajax request with .then()
method and handle the case when the request is successful.
const handleSubmit = event => {
event.preventDefault();
const data = { email, message, time: getTime() };
Axios.post('https://us-central1-ourprojectname.cloudfunctions.net/submit', data)
.then(res => {
// here will be code
})
.catch(error => {
console.log(error);
});
};
Don't forget the Firebase's database import:
import 'firebase/database';
Our barebone method will look something like:
const handleSubmit = event => {
event.preventDefault();
const data = { email, message, time: getTime() };
Axios.post('https://us-central1-ourprojectname.cloudfunctions.net/submit', data)
.then(res => {
if (firebase) {
return firebase
.database()
.ref('contacts')
.push(data);
}
})
.catch(error => {
console.log(error);
});
};
This will create a table called contacts
and push the data
object to the Firebase Database.
At the end we have a working Contact form which will notify you via email and save a history of all the entries in a database.
Final code: functions/index.js
'use strict';
const functions = require('firebase-functions');
const nodemailer = require('nodemailer');
const cors = require('cors')({ origin: true });
const gmailEmail = functions.config().gmail.email;
const gmailPassword = functions.config().gmail.password;
const mailTransport = nodemailer.createTransport({
service: 'gmail',
auth: {
user: gmailEmail,
pass: gmailPassword
}
});
exports.submit = functions.https.onRequest((req, res) => {
cors(req, res, () => {
if (req.method !== 'POST') {
return;
}
const mailOptions = {
from: req.body.email,
replyTo: req.body.email,
to: gmailEmail,
subject: `from my website ${req.body.email}`,
text: req.body.message,
html: `<p>${req.body.message}`
};
mailTransport.sendMail(mailOptions);
res.status(200).send({ isEmailSend: true });
});
});