Email marketing is a fundamental part of almost any SaaS or business these days, so whether you want to expand your newsletter audience or grow your pre-launch waitlist, this project is for you.
The code for this tutorial is available on GitHub
Getting Started
To create a new Next.js TypeScript application, type the following commands in the terminal and cd into your project’s folder.
npx create-next-app@latest --ts
# or
yarn create next-app --typescript
For the component library, we will use Chakra UI.
Run the following command to add Chakra UI to our project.
npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^6
# or
yarn add @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^6
After installing Chakra UI, you need to set up the ChakraProvider at the root of your application.
// pages/_app.tsx
import { ChakraProvider } from '@chakra-ui/react'
import type { AppProps } from 'next/app';
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<ChakraProvider>
<Component {...pageProps} />
</ChakraProvider>
);
}
Now, let’s clean our app a little bit.
Remove the styles folder and all related imports from the pages/_app.tsx & pages/index.tsx files, and clean the “Home” component.
// pages/index.tsx
export default function Home() {
return (
<>Home Page</>
);
}
Why Chakra UI
First of all, let’s explain what Chakra UI is and why we are going to use it.
Chakra UI is a component-based library. It’s made up of basic building blocks that can help you build web applications faster.
The reasons for using Chakra UI:
- Accessibility – Chakra UI strictly follows WAI-ARIA standards for all components.
- Themeable – You can customize any part of the components to match your design needs.
- Developer Experience – It’s extremely simple to use and once you get it, it’s super fast to build and style with.
- Fun – Yep, I said it. Chakra UI is fun.
- There are more reasons which you can check here.
Let’s Build Our UI
First, we’ll create a simple layout containing our form, connect a state with our email input and add a handleFormSubmit function for email submission.
The syntax is straightforward:
// pages/index.tsx
import { Button, Flex, Input, Text } from "@chakra-ui/react";
export default function Home() {
const [emailInput, setEmailInput] = useState('');
const [buttonLoading, setButtonLoading] = useState(false);
const handleFormSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
// TODO: handle submission logic
};
return (
<form onSubmit={handleFormSubmit}>
<Flex gap='15px'>
<Input
type='email'
placeholder="Enter your email..."
value={emailInput}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setEmailInput(e.target.value)}
/>
<Button isLoading={buttonLoading} type='submit' bg='purple.500' color='white' _hover={{ bg: 'purple.600' }} _active={{ bg: 'purple.700' }}>
Subscribe
</Button>
</Flex>
<Text color='gray.500' as='small'>Join our pre-launch waitlist!</Text>
</form>
);
}
And now, to make it look good and mobile friendly we’ll add some Chakra UI magic and wrap the form we created so far:
// pages/index.tsx
import { ChangeEvent, FormEvent, useState } from "react";
import { Button, Center, Container, Flex, Heading, Input, Text } from "@chakra-ui/react";
import Image from "next/image";
export default function Home() {
const [emailInput, setEmailInput] = useState('');
const [buttonLoading, setButtonLoading] = useState(false);
const handleFormSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
// TODO: handle submission logic
};
return (
<Center>
<Flex gap='50px' justifyContent='space-between' alignItems='center' flexDir={['column', null, 'row']} maxW='1200px' m='50px 0'>
<Container>
<Heading fontSize={['4xl', null, null, '5xl']} mb='20px'>Be the first to know when we launch</Heading>
<Text color='gray.600' fontSize='18px' mb='30px'>
We are still building. Subscribe for updates and 20% off when
we launch. No spam, we promise!
</Text>
<form onSubmit={handleFormSubmit}>
<Flex gap='15px'>
<Input
type='email'
placeholder="Enter your email..."
value={emailInput}
onChange={(e: ChangeEvent<HTMLInputElement>) =>
setEmailInput(e.target.value)}
/>
<Button isLoading={buttonLoading} type='submit' bg='purple.500' color='white' _hover={{ bg: 'purple.600' }} _active={{ bg: 'purple.700' }}>
Subscribe
</Button>
</Flex>
<Text color='gray.500' as='small'>Join our pre-launch waitlist!</Text>
</form>
</Container>
<Container display='flex' justifyContent='center'>
<Image src={'/hero-image.png'} alt='app mockup' width={470} height={470} />
</Container>
</Flex>
</Center>
);
}
And voilà, our UI is ready:
Mailchimp Set Up
To integrate with Mailchimp there are a few steps we’ll need to take:
- Create an account.
- Get your Mailchimp API key.
- Get your Mailchimp Audience ID.
If you don’t already have an account, go ahead and create one: Mailchimp.
Now, to get your API key, click your account avatar and go to your account profile page.
On the account profile page, click the “Extras” tab, and select “API Keys” option:
Scroll down, and click the “Create A Key” button to get your API key:
Next, to get our Audience ID, we’ll navigate to “All contacts” under “Audience” in the side menu.
There you will click the “Settings” tab, and select the “Audience name and defaults” option:
And here it is, under “Audience ID”:
At last, let’s create a .env.local file at the root of our project, and place both the API key and the Audience ID.
And that’s it for the Mailchimp setup.
#.env.local
MAILCHIMP_API_KEY= my-api-key
MAILCHIMP_AUDIENCE_ID= 7baac126cc
Building the API
In this part, we will create the API to handle user signing into our mailing list.
Let’s start by creating subscribe.ts file inside theapi folder, located under the pages directory, and add a simple handler function with try & catch:
// pages/api/subscribe.ts
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
// TODO: handle subscription.
} catch (e) {
res.status(401).json({error: 'Something went wrong, please try again later.'})
}
}
The API endpoint we are going to use to add new subscribers is the /lists/{list_id} endpoint. You can check it out here.
Now, in the subscribe.ts route we created before, we are going to make a POST request and add a new subscriber to our list:
// pages/api/subscribe.ts
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { email } = JSON.parse(req.body);
if (!email) {
res.status(401).json({ error: 'Email is required' });
return;
}
const mailChimpData = {
members: [
{
email_address: email,
status: 'subscribed'
}
]
}
try {
const audienceId = process.env.MAILCHIMP_AUDIENCE_ID as string
const URL = `https://us1.api.mailchimp.com/3.0/lists/${audienceId}`
const response = await fetch(URL,
{
method: 'POST',
headers: {
Authorization: `auth ${process.env.MAILCHIMP_API_KEY as string}`,
},
body: JSON.stringify(mailChimpData),
}
);
const data = await response.json()
// Error handling.
if (data.errors[0]?.error) {
return res.status(401).json({ error: data.errors[0].error });
} else {
res.status(200).json({ success: true});
}
} catch (e) {
res.status(401).json({error: 'Something went wrong, please try again later.'})
}
}
Above, we made an API POST request to Mailchimp’s /lists/{list_id} endpoint.
The mailChimpData object we are sending in the request body, is the subscriber/s data Mailchimp expects to get for adding a new member to our list.
We are also using our API key in the Authorization header, without it, the request won’t work.
Connecting the Client
To connect our client with the API we built, there is one last thing we need to do.
Remember the component we built earlier in this post?
Well, there’s a handleFormSubmit function we still haven’t written.
We’ll now send the user’s email from the form we built to the API we just wrote and we will also use Chakra’s useToast hook to display success and error notifications:
// pages/index.tsx
import { ChangeEvent, FormEvent, useState } from "react";
import { Button, Center, Container, Flex, Heading, Input, Text, useToast } from "@chakra-ui/react";
import Image from "next/image";
export default function Home() {
const [emailInput, setEmailInput] = useState('');
const [buttonLoading, setButtonLoading] = useState(false);
const toast = useToast();
const handleFormSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!emailInput) {
return toast({
description: 'Email is required',
status: 'error'
});
}
setButtonLoading(true);
try {
const res = await fetch('/api/subscribe', { method: 'POST', body: JSON.stringify({ email: emailInput }) });
const data = await res.json();
if (data.success) {
toast({
title: 'Joined successfully.',
description: "Thank you for joining the waitlist!",
status: 'success'
});
} else {
throw new Error(data?.error || 'Something went wrong, please try again later');
}
} catch(e) {
toast({
description: (e as Error).message,
status: 'error'
});
}
setEmailInput('');
setButtonLoading(false);
};
return (
//The logic we wrote earlier.
}
Now, let’s see if it works.
And it does, woohoo 🎉🥳.
Summary
In this post, we’ve covered how to collect emails using Next.js with Mailchimp as well as the basics of Chakra UI.
You can view the GitHub code here.
Thanks for reading 🙃.
Jonathan is a software developer at Common Ninja by day and a lover of all things nerdy by night. When he’s not busy coding, you can find him geeking out over the latest video game or eating something.