Sending Emails
Something a lot of applications will eventually have to do is send emails. To demonstrate how you can do that with RedwoodJS we're going to build a simple list of users and their email addresses, and allow you to trigger an email to them. We'll also include some auditing features, so you get a history of emails you sent to your users. The audit logs will be implemented by using one service from within another service — a powerful RedwoodJS feature.
The emails will be sent using the npm package nodemailer together with SendInBlue.
Setup
The first thing to do is to create a new RedwoodJS project.
yarn create redwood-app --typescript email
When that's done, go into the email
directory and install the nodemailer
package.
yarn workspace api add nodemailer
DB design
Now, fire up your editor of choice and find the schema.prisma
file and remove the example model. The app we're building is going to have two models. One for our users and one for the audit logs. Paste the following two models in your schema file.
model User {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
email String @unique
name String?
audits Audit[]
}
model Audit {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
userId String
user User @relation(fields: [userId], references: [id])
log String
}
Technically all we really need in the User model is the email address and the Audit relation field. But personally I have never regretted having an id, and the two timestamps in my models. But I have regretted not having them, having to go back to add them later. So now I always include them from the start. And I also added a name
field to the user, to make this example at least a little bit realistic 😁. A proper user model would most likely have way more fields. The audit model is also overly simplistic. Especially the single log
string. A proper audit trail needs way more info. But for demo purposes it's good enough. Final thing I wanted to mention was the relation. We set up a one-to-many relation from the user to the audit logs so that we can easily find all logs belonging to a user by simply following the relation.
Now we can go ahead and migrate our database and create the SDLs and services needed to interact with the Prisma model using GraphQL.
yarn rw prisma migrate dev --name email
Scaffold
One of Redwood's stand-out features is the scaffolds. We'll be using scaffolds here to quickly get a nice visual list of the users in our database to work with.
yarn rw g scaffold User
Let's do it for Audit as well
yarn rw g scaffold Audit
Now let's run the Redwood dev server to see what we've created so far.
yarn rw dev
Your web browser should open up and show the default Redwood app home page with a list of links to all your pages. Click on the /users
link and then go ahead and create a few users. Since we're going to send emails to these users, use emails you can actually check. So you can make sure it works. A service I like to use for generating random users with real email addresses is https://www.fakenamegenerator.com. Just click the link on that page to activate the email address and you'll be able to send emails from your app, and see them arrive.
So if you create three users you should see something like this
Clicking to show the details on one of the users you should see a page similar to what I have below here. To that page I've also added a button to send an email to the user. I'll show you how next!
Button to send email
To add our button, and the actions connected to it, we need to add a fair bit of code to the User component. I've put the full code below to make sure you don't miss anything.
import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import { Link, routes, navigate } from '@redwoodjs/router'
const DELETE_USER_MUTATION = gql`
mutation DeleteUserMutation($id: String!) {
deleteUser(id: $id) {
id
}
}
`
const EMAIL_USER_MUTATION = gql`
mutation EmailUserMutation($id: String!) {
emailUser(id: $id) {
id
}
}
`
const timeTag = (datetime) => {
return (
<time dateTime={datetime} title={datetime}>
{new Date(datetime).toUTCString()}
</time>
)
}
const User = ({ user }) => {
const [deleteUser] = useMutation(DELETE_USER_MUTATION, {
onCompleted: () => {
toast.success('User deleted')
navigate(routes.users())
},
onError: (error) => {
toast.error(error.message)
},
})
const [emailUser] = useMutation(EMAIL_USER_MUTATION, {
onCompleted: () => {
toast.success('Email sent')
},
onError: (error) => {
toast.error(error.message)
},
})
const onDeleteClick = (id) => {
if (confirm('Are you sure you want to delete user ' + id + '?')) {
deleteUser({ variables: { id } })
}
}
const onEmailClick = (user) => {
if (confirm(`Are you sure you want to send an email to ${user.name}?`)) {
emailUser({ variables: { id: user.id } })
}
}
return (
<>
<div className="rw-segment">
<header className="rw-segment-header">
<h2 className="rw-heading rw-heading-secondary">
User {user.id} Detail
</h2>
</header>
<table className="rw-table">
<tbody>
<tr>
<th>Id</th>
<td>{user.id}</td>
</tr>
<tr>
<th>Created at</th>
<td>{timeTag(user.createdAt)}</td>
</tr>
<tr>
<th>Updated at</th>
<td>{timeTag(user.updatedAt)}</td>
</tr>
<tr>
<th>Email</th>
<td>{user.email}</td>
</tr>
<tr>
<th>Name</th>
<td>{user.name}</td>
</tr>
</tbody>
</table>
</div>
<nav className="rw-button-group">
<Link
to={routes.editUser({ id: user.id })}
className="rw-button rw-button-blue"
>
Edit
</Link>
<button
type="button"
className="rw-button rw-button-red"
onClick={() => onDeleteClick(user.id)}
>
Delete
</button>
<button
type="button"
className="rw-button rw-button-blue"
onClick={() => onEmailClick(user)}
>
Send email
</button>
</nav>
</>
)
}
export default User
We're using a GraphQL mutation here to trigger the sending of the email. To make that mutation work we need to add it to the users SDL.
export const schema = gql`
// ...
type Mutation {
// ...
emailUser(id: String!): User! @requireAuth
}
`
And then in the users service we'll just create a dummy method to start with.
// ...
import type { Prisma } from '@prisma/client'
// ...
export const emailUser = async ({ id }: Prisma.UserWhereUniqueInput) => {
const user = await db.user.findUnique({
where: { id },
})
console.log('Sending email to', user)
return user
}
// ...
Now is a good time to go get a fresh cup of coffee, or other beverage of choice. When you come back we'll create an account at SendInBlue and use the credentials from there to send an email.