Recently, I publicly shared a project that I’m working on, MapleStack, a platform for connecting companies everywhere to tech talent in Canada.
Today, I’ll like to share how I built MapleStack’s waitlist form in minutes.
The Problem
I need to create a simple form that collects user information, stores it somewhere and notifies the user via email.
The Solution
Although it is a simple problem to reason about, I like the fact that it touches on major features that make up a web application. To get this working we need to do the following:
- Frontend: Create a user form, input validation, error handling on the client
- Backend: Input validation, Data storage, error handling
- Notification: Email delivery service
Additionally, we still need to deploy the app, configure the endpoint, DNS records, configure TLS certificates, and probably a few other things.
We can already see how a “simple” task suddenly contains a lot of moving parts.
My options? Pay for a Service or build.
You are here because I chose the latter.
To get this done quickly, I decided to design this using familiar tools
- Frontend: React form component
- Backend: AWS API Gateway + AWS Lambda
- Storage: AWS DynamoDB
- Notification: AWS SES
Leveraging AWS products helps me get setup quickly. I don’t need to worry about configuring application servers, email servers and more. Now, I just need to write the code for the frontend form, processing the user input and sending off the notification.
Coding with AI: ChatGPT vs Bard
I missed the major hype cycle and only started using ChatGPT a few weeks back. I’ve heard the litany of things that it can get done so I decided to give it a test.
With no knowledge of prompts, I decided to just talk to it as if I was thinking about the task that needs to be done and it sort of worked. After a few iterations, here are the final prompts.
The Prompt – React Form
Write a React component that is used to collect user information to join a waitlist. It should satisfy the following criteria
1. Code is in Typescript.
2. Collect these details: Name, Email, Usage with two options candidate or employer
3. All inputs are required, Show error messages if when errors occur.
4. Use react-hook-form to manage the form
5. Use @tanstack/react-query to submit the details to an endpoint(TBD)
6. Use Tailwind CSS for styling
That’s all I can think of for now. Do you have any questions or suggestions to add?
Response – ChatGPT
import React from 'react';
import { useForm } from 'react-hook-form';
import { useMutation } from '@tanstack/react-query';
interface WaitlistFormData {
name: string;
email: string;
usage: 'candidate' | 'employer';
}
const WaitlistForm: React.FC = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<WaitlistFormData>();
const mutation = useMutation((data: WaitlistFormData) => {
// Call your API endpoint with the form data
console.log(data);
// Replace the console.log with the actual API call
// For example: axios.post('/api/waitlist', data);
});
const onSubmit = handleSubmit((data) => {
mutation.mutate(data);
});
return (
<form onSubmit={onSubmit} className="max-w-md mx-auto">
<div className="mb-4">
<label htmlFor="name" className="block font-medium text-gray-700">
Name
</label>
<input
type="text"
id="name"
{...register('name', { required: 'Name is required' })}
className={`form-input mt-1 block w-full ${
errors.name ? 'border-red-500' : 'border-gray-300'
}`}
/>
{errors.name && (
<p className="text-red-500 text-sm mt-1">{errors.name.message}</p>
)}
</div>
<div className="mb-4">
<label htmlFor="email" className="block font-medium text-gray-700">
Email
</label>
<input
type="email"
id="email"
{...register('email', {
required: 'Email is required',
pattern: {
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
message: 'Invalid email address',
},
})}
className={`form-input mt-1 block w-full ${
errors.email ? 'border-red-500' : 'border-gray-300'
}`}
/>
{errors.email && (
<p className="text-red-500 text-sm mt-1">{errors.email.message}</p>
)}
</div>
<div className="mb-4">
<label htmlFor="usage" className="block font-medium text-gray-700">
Usage
</label>
<select
id="usage"
{...register('usage', { required: 'Usage is required' })}
className={`form-select mt-1 block w-full ${
errors.usage ? 'border-red-500' : 'border-gray-300'
}`}
>
<option value="">Select usage</option>
<option value="candidate">Candidate</option>
<option value="employer">Employer</option>
</select>
{errors.usage && (
<p className="text-red-500 text-sm mt-1">{errors.usage.message}</p>
)}
</div>
<button
type="submit"
className="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded"
>
Join Waitlist
</button>
</form>
);
};
export default WaitlistForm;
Code language: JavaScript (javascript)
Response – Bard
import { useQuery } from '@tanstack/react-query';
import React, { useEffect, useState } from 'react';
import { Field, Form, Formik } from 'react-hook-form';
import { tailwind } from 'tailwindcss';
const WaitlistForm = () => {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [usage, setUsage] = useState('');
const query = useQuery('waitlist', async () => {
return await fetch('/api/waitlist', {
method: 'POST',
body: JSON.stringify({
name,
email,
usage,
}),
});
});
useEffect(() => {
if (query.isSuccess) {
alert('Thank you for joining the waitlist!');
}
}, [query.isSuccess]);
return (
<Formik
initialValues={{ name, email, usage }}
onSubmit={async (values) => {
await query.mutate(values);
}}
>
<Form>
<Field
name="name"
type="text"
label="Name"
placeholder="Your name"
required
error={name === '' ? 'Name is required' : undefined}
/>
<Field
name="email"
type="email"
label="Email"
placeholder="Your email address"
required
error={email === '' ? 'Email is required' : undefined}
/>
<Field
name="usage"
type="select"
label="Usage"
required
options={[
{ value: 'candidate', label: 'Candidate' },
{ value: 'employer', label: 'Employer' },
]}
/>
<button type="submit">Join Waitlist</button>
</Form>
</Formik>
);
};
export default WaitlistForm;
Code language: TypeScript (typescript)
To be succinct, I was impressed! Although Bard generated some incorrect code, the majority of it was still usable. ChatGPT on the other hand seems to have provided a more complete output.
Armed with this code, I decided to do the same for the backend.
The Prompt – Node JS lambda function
Write an AWS Lambda NodeJS function that satisfies the following conditions:
1. It will be called by the API Gateway.
2. The function should save the data to a DynamoDB table.
3. The expected input is a JSON object with the following properties:
a. name
b. email
c. usage
4. It should validate the input. The name and email should be required. The email should be a valid email address. The usage should be one of the following values: candidate, employer.
5. Using SES, it should send a confirmation email to the user, thanking them for joining the waitlist. The email should contain the following information:
a. Name
b. Email
c. Usage
6. The function should return a JSON object with the following properties:
a. success
b. message
7. The function should return a 400 error if the input is invalid.
8. The function should return a 500 error if there is an internal error.
9. The function should return a 200 if the data was saved successfully.
10. It should handle OPTIONS http method.
11. It should handle POST http method.
12. DynamoDB table name should be passed as an environment variable.
13. SES email address should be passed as an environment variable
That’s all I can think of for now. Do you have any questions or suggestions to add?
Response – ChatGPT
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();
const ses = new AWS.SES();
const dynamoDBTableName = process.env.DYNAMODB_TABLE_NAME;
const sesEmailAddress = process.env.SES_EMAIL_ADDRESS;
exports.handler = async (event) => {
try {
// Handle OPTIONS request for CORS preflight
if (event.httpMethod === 'OPTIONS') {
return createResponse(200, { success: true, message: 'Preflight request handled successfully' });
}
// Handle POST request
if (event.httpMethod === 'POST') {
const { name, email, usage } = JSON.parse(event.body);
// Validate the input
if (!name || !email || !usage || !validateEmail(email) || !validateUsage(usage)) {
return createResponse(400, { success: false, message: 'Invalid input' });
}
// Save the data to DynamoDB
await saveDataToDynamoDB(name, email, usage);
// Send confirmation email using SES
await sendConfirmationEmail(name, email, usage);
return createResponse(200, { success: true, message: 'Data saved and email sent successfully' });
}
// Handle unsupported HTTP methods
return createResponse(405, { success: false, message: 'Method Not Allowed' });
} catch (error) {
console.error('Error:', error);
return createResponse(500, { success: false, message: 'Internal server error' });
}
};
const validateEmail = (email) => {
// Basic email validation using regular expression
const emailRegex = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i;
return emailRegex.test(email);
};
const validateUsage = (usage) => {
// Check if usage is one of the allowed values
const allowedUsageValues = ['candidate', 'employer'];
return allowedUsageValues.includes(usage);
};
const saveDataToDynamoDB = async (name, email, usage) => {
const params = {
TableName: dynamoDBTableName,
Item: { name, email, usage },
};
await dynamodb.put(params).promise();
};
const sendConfirmationEmail = async (name, email, usage) => {
const subject = 'Thank you for joining the waitlist';
const message = `Thank you for joining the waitlist!\n\nName: ${name}\nEmail: ${email}\nUsage: ${usage}`;
const params = {
Destination: { ToAddresses: [email] },
Message: {
Body: { Text: { Data: message } },
Subject: { Data: subject },
},
Source: sesEmailAddress,
};
await ses.sendEmail(params).promise();
};
const createResponse = (statusCode, body) => {
return {
statusCode,
body: JSON.stringify(body),
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': true,
},
};
};
Code language: JavaScript (javascript)
Response – Bard
const AWS = require("aws-sdk");
const DynamoDB = new AWS.DynamoDB();
const SES = new AWS.SES();
exports.handler = async (event, context) => {
const { name, email, usage } = JSON.parse(event.body);
// Validate the input data
if (!name || !email || !usage) {
return {
success: false,
message: "Invalid input data",
};
}
// Check if usage is valid
const validUsages = ["candidate", "employer"];
if (!validUsages.includes(usage)) {
return {
success: false,
message: "Invalid usage",
};
}
// Save the data to DynamoDB
const params = {
TableName: process.env.DYNAMODB_TABLE_NAME,
Item: {
Name: name,
Email: email,
Usage: usage,
},
};
await DynamoDB.putItem(params);
// Send a confirmation email
const params = {
Destination: {
ToAddresses: [email],
},
Message: {
Subject: "Thank you for joining the waitlist",
Body: {
Text: {
Data: `Thank you for joining the waitlist. We will notify you when we are ready to launch.
Name: ${name}
Email: ${email}
Usage: ${usage}`,
},
},
},
};
await SES.sendEmail(params);
// Return a success message
return {
success: true,
message: "Data saved successfully",
};
};
// This function is used to handle the OPTIONS http method.
exports.options = async (event, context) => {
return {
statusCode: 200,
};
};
// This function is used to handle the POST http method.
exports.post = async (event, context) => {
return exports.handler(event, context);
};
Code language: JavaScript (javascript)
Impressed again!
Thoughts
- For this task, ChatGPT seemed to produce better code structure than Bard.
- ChatGPT asked clarifying questions. This probably helped it generate more relevant code.
- Bard didn’t ask clarifying questions and just produced an output it felt was appropriate.
- The code provided was good enough and only need a few edits from me. Mainly extra checks and validation.
- It reduced development time to minutes!
- It’s not bulletproof. The code still had errors, the most common was related to using the function in a deprecated or wrong way.
I know there has been a lot of talk about AI and replacing programmers and after using it, I think it’s more of an assistant. Will it make us redundant? Maybe, some day.. but for now it’s helping me get things done faster and that’s amazing!
If you have not already done so, check out MapleStack and join the waitlist that was built using the code above.
Also, follow MapleStack’s Twitter and LinkedIn pages for updates.
As always, feel free to reach out if you have any questions or just for a chat 🙂