The Build Series: E1 — Contact Form powered by Firebase Functions
- Published on
Welcome to the first episode of The Build Series where we will build a customized contact form and when the user submits the form, it will add all the form information to Google Sheets. At the backend, we’ll write a NodeJS function and deploy it to Firebase Functions which will update the Google Sheet for us. Let’s get started.
Setup Firebase & CLI tools
First, let’s set up a Firebase Project to deploy our functions.
- Open your Firebase Console and Click on ‘Add Project’
- Give it a name. Let’s call it ‘contact-form-api’
- Install the Firebase CLI on your local machine using npm by running the install command in our terminal
npm install -g firebase-tools
- This command installs the globally available
firebase
command - Sign in to Firebase using your Google account by running
firebase login
This command connects your local machine to Firebase and grants you access to your Firebase projects.
To test that authentication worked (and to list all of your Firebase projects), run the following command:
firebase list
The above process is pretty straightforward. You can find more details regarding the installation in the docs.
Setting up the Project
Open a terminal and create a new directory
mkdir contact-form-api && cd contact-form-api
Let’s initialize a Firebase project here
firebase init
Select the option Functions: Configure and deploy Cloud Functions
- Select the project we created in the first step contact-form-api
- Next, select the language. We’re going to go with TypeScript here. You can select JavaScript if you prefer.
- Let the CLI install all the dependencies for us.
- Let’s open the folder in our IDE.
Setting up the Contact Sheet
- Open Google Drive and create a new Google Sheet, let’s call it Contact Sheet
- Add 3 columns, Name, Email Address, Message
- Notice the URL, we need to get the ID of the sheet from the URL. We’ll be using this ID to write to the Sheet
https://docs.google.com/spreadsheets/d/<COPY_THIS_ID_>/edit#gid=0
Configuring Auth
To edit or update Google Sheets we need user auth. However, since our Google account is configured with 2FA (if yours isn’t, then do it right now)so we need to use Service Account Keys instead.
- Get a service account key from https://console.cloud.google.com/apis/credentials (You can also restrict what the key is allowed to do)
When creating, make sure you click the Furnish a new private key.
Select JSON
when it asks you how to download the key.
- The
service account key
we have just generated includes aclient_email.
- Open the Contact Sheet sharing options, and allow this
client_email
to have write access to this document.
For more information on Service Account Keys
refer the docs.
Set up the Function
Open the project and install the following packages:
npm install express googleapis
We can do this without express, but we want to add more than one endpoint to this function. If you want to skip express you can do that.
Now let’s code the express server,
import * as functions from 'firebase-functions'
const { google } = require('googleapis')
const express = require('express')
const cors = require('cors')
const app = express()
// Automatically allow cross-origin requests
app.use(cors({ origin: true }))
const privatekey = require('./private-key.json')
// configure a JWT auth client
const jwtClient = new google.auth.JWT(privatekey.client_email, null, privatekey.private_key, [
'https://www.googleapis.com/auth/spreadsheets',
])
const spreadsheetId = 'SHEET_ID'
const sheetName = 'Sheet1'
const sheets = google.sheets('v4')
const authorizeAndDoNext = async () => {
await jwtClient.authorize()
console.log('Successfully Conected')
}
export const ping = (request, response) => {
response.send('Hello from the other side')
}
/**
* Write data to sheet
* @param data {name,email,message}
*/
export const writeToSheet = async (data) => {
return await sheets.spreadsheets.values.append({
auth: jwtClient,
spreadsheetId: spreadsheetId,
range: sheetName,
valueInputOption: 'RAW',
insertDataOption: 'INSERT_ROWS',
resource: {
values: [[data.name, data.email, data.message]],
},
})
}
/**
* The secret sauce.
*/
export const contact = async (request, response) => {
try {
await authorizeAndDoNext()
const result = await writeToSheet(request.body)
response.status(200).send(result.data)
} catch (error) {
console.log('Error', error)
response.status(400).send(error)
}
}
app.get('/ping', (req, res) => res.send('Hello from the other side'))
app.post('/contact', contact)
export const api = functions.https.onRequest(app)
What we are doing here is whenever we get a POST
request from the user at the /api/contact
endpoint,
- We authorize our server using the
Service Account Key,
- When the auth is successful, we use the token we got and append the data to the Contact Sheet.
We have also added another endpoint called /api/ping
which we will use to verify if we are able to hit the functions from our local machine.
Test the Function
We will first test the function locally. To do that let’s open the terminal and run the script:
npm run serve
You’ll need to run this inside the functions
directory where there is a package.json
file.
We should see that the functions are ready to run, with a message in the terminal prompt:
✔ functions: api: http://localhost:5000/contact-form-api/us-central1/api
Open terminal and test the URL:
curl http://localhost:5000/contact-form-api/us-central1/api/ping
We should see Hello from the other side
in the response. This means we have configured express correctly.
Now let’s test POST
with some form data.
Change the URL to http://localhost:5000/contact-form-api/us-central1/api/contact
with json data
{ "name": "John Doe", "email": "john@company.com", "message": "Test Message" }
It should look something like this:
curl -X POST \
http://localhost:5000/contact-form-api/us-central1/api/contact \
-H 'Content-Type: application/json' \
-d '{
"name": "John Doe",
"email": "john@doe.me",
"message": "Hey There!"
}'
We should get the response from the API as:
{
"spreadsheetId": "SHEET_ID",
"tableRange": "Sheet1!A1:C6",
"updates": {
"spreadsheetId": "SHEET_ID",
"updatedRange": "Sheet1!A7:C7",
"updatedRows": 1,
"updatedColumns": 3,
"updatedCells": 3
}
}
If you get any auth related issues, double check the private_key.json
file. If you get any issues such as cannot write to sheet, check if sharing is enabled for the sheet with the email provided in private_key.json.
Build the Form
Now that we've built the backend, let’s build a simple contact form. I'm going to keep everything in one file for the sake of simplification.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Contact Form</title>
</head>
<body>
<form id="form">
<div class="form-group">
<label for="name">Name</label>
<input type="text" name="name" class="form-control" id="name" placeholder="Enter Name" />
</div>
<div class="form-group">
<label for="email">Email address</label>
<input
type="email"
name="email"
class="form-control"
id="email"
placeholder="Enter email"
/>
<small id="emailHelp" class="form-text text-muted"
>We'll never share your email with anyone else.</small
>
</div>
<div class="form-group">
<label for="message">Message</label>
<input type="text" name="message" class="form-control" id="message" placeholder="Message" />
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<script>
var formEl = document.getElementById('form')
formEl.addEventListener('submit', function (event) {
var headers = new Headers()
headers.set('Accept', 'application/json')
var formData = new FormData(event.target)
var object = {}
formData.forEach(function (value, key) {
object[key] = value
})
var json = JSON.stringify(object)
var url = 'FUNCTIONS_URL'
var fetchOptions = {
method: 'POST',
headers,
body: json,
}
var responsePromise = fetch(url, fetchOptions)
responsePromise.then(function (response) {
// Message Indicating Successful Submission
})
event.preventDefault()
})
</script>
</body>
</html>
We built a simple form here which uses the fetch()
api to post data to our firebase function. Replace the url in the javascript file and test it out.
Deploying Functions
Now that the Form and the Function are built, it is time to deploy the functions.
Open a terminal within the functions directory and execute npm run deploy
. This will take a couple of minutes. If successful, you should see something like this in the terminal.
npm run deploy
✔ functions: Finished running predeploy script.
i functions: ensuring necessary APIs are enabled...
✔ functions: all necessary APIs are enabled
i functions: preparing functions directory for uploading...
i functions: packaged functions (54.02 KB) for uploading
✔ functions: functions folder uploaded successfully
i functions: creating Node.js 6 function api(us-central1)...
✔ functions[api(us-central1)]: Successful create operation.
Function URL (api): https://us-central1-contact-form-api.cloudfunctions.net/api
✔ Deploy complete!
Project Console: https://console.firebase.google.com/project/contact-form-api/overview
We can confirm if the deployment is successful by hitting our /ping
endpoint.
Bonus Points
If you’ve already configured Firebase Hosting, then you can use the same project to deploy functions to and have it accessed using http://yourdomain.com/api/ping.
To do that, add the following lines to your firebase.json
file.
{
"hosting": {
"public": "dist",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [
{
"source": "/api/**",
"function": "api"
}
]
}
}
The name of the function is what we exported from our index.ts
functions file.
End Points
We did quite a lot in this episode.
- Configured a Project in Firebase Console
- Installed Firebase CLI
- Created a function using NodeJS
- Created a simple contact form
- Connected the contact form and the contact sheet using the function
Now that we’ve built the foundation, we can also
- Add validation so that same email doesn’t go in the form more than once,
- Enable CORS, so only our domain can post data,
- Enable Authentication for our functions,
- Send out an email using
nodemailer
thanking the subscriber, - and a lot more…
We have reached the end of this episode of The Build Series. Reach out in case of any issues/queries.
Until next time! ✌🏽
On this page