Now that the server is ready, we can take a step further into making our frontend live by consuming the API.
Earlier the design team provided us the designs for each authentication screens (you can check them here). We will bring them into code, and make our frontend handle authentication via our GraphQL API. For that we will:
We created a persistent zustand store useAuthStore
that has the following properties:
auth
– The object where we store the user’s details extracted from the decoded JWT accessToken
.accessToken
– The encoded access token.setTokens(accessToken)
– A function where we receive the encoded access token, decode it, and store the decoded contents in auth
, and the encoded accessToken
itself.reset
– A function that resets the auth store, used on logout.First, we set up apolloClient
with the appropriate links:
httpLink
– This is where we configure the GraphQL endpoint, using import.meta.env.VITE_API_URL
which is an environment variable we set in the .env
file.authLink
– This link is responsible for making sure we always supply a valid access token for signed in users in every request. Here’s how it works:refreshToken
(stored in HTTP only cookies) to generate a new accessToken
via our REST API endpoint /auth/refreshTokens
.accessToken
in useAuthStore
, and continue with our request to the GraphQL API.errorLink
(added with the toast message panel ticket) – Responsible for handling network errors (if the servers are unavailable, or the user has weak internet connection), and giving feedback to the user via toast messages.We created an util function logout
that:
Logout
GraphQL mutation, removing the refreshToken
from the database, invalidating it.apolloClient
cache.Our API intelligently lets the frontend know when the user needs to be logged out if they try to access an endpoint they don’t have access to via the extensions.doLogout
flag. We created custom hooks that log the user out with the above logout
function if we are notified about it.
The hooks are:
useQuery
useLazyQuery
useMutation
graphql-codegen
This may be familiar as we used codegen in our API to generate types for our resolvers, and in @repo/graphql
to generate the schema types.
We are using it in our web app to generate hooks using the above custom hooks as a base, so we don’t have to manually define each query with their types. We reference the queries from @repo/graphql/queries
, so every query will have a custom hook generated with types in place.
We will see this in action when implementing the authentication screens.
As per the designs we will have to provide users with feedback across different screens about the API responses. These include error messages, and success messages like letting the users know after setting their passwords that they can use their new credentials on the sign in screen.
We created a centralised zustand store for the messages, so they can be accessed from any component.
When messages are created, they can be specified with:
type
– The type of the message, can be:success
error
warning
info
id
– Identifies the message, so messages with the same id
won’t be duplicated. If not specified, they receive a random uuid
.text
– The content of the message.href
– Optional, users will be redirected to the specified location if provided when clicking the toast card.expireIn
– Optional, when set, the message will automatically disappear after the specified time (in milliseconds).The messages can be created with the addMessages
method of the store, which accepts an array of message inputs specified as above. Every message stored will be assigned a remove
function which removes itself from the list when called.
The store is storing the messages in messages
, and every message can be removed with the store’s removeMessages
method.
Following the designs, we created a floating scrollable toast panel with fixed position on the bottom left corner of the screen.
We made sure that the panel won’t make the site inaccessible on smaller devices as the max height is 60% of the device height, and max lg
wide.
On top of the messages a Clear button appears that removes all messages.
To provide meaningful error messages on request failures, we extended our custom query and mutation hooks so when an error occurs it automatically creates error messages to the store.
We also added a custom option preventToastError
to the hooks which when set to true
, skips adding new messages to the store, allowing for custom error handling inside the component.
Now that we wired the frontend and API together, it’s time to implement screens. We will start with the sign in screen.
We created the SignIn
component. It allows users to enter their emails and passwords to authenticate themselves so we can show their content in the app. Here’s a summary of its key features:
react-hook-form
with zod
for schema-based form validation.useSignInMutation
to send login credentials to the backend.useAuthStore
which will extract user information from it, authenticating the user.IoEye
and IoEyeOff
icons).react-i18next
to support multilingual text in both the form and confirmation message.We extended our existing routing utility (ROUTES
), adding all necessary route paths as variables for authentication:
SIGN_UP
SIGN_IN
SET_PASSWORD({passwordTokenId})
RESET_PASSWORD
VERIFY_EMAIL({verificationTokenId})
Some of these are constants, storing the path as string (like SIGN_UP
resolving to /signUp
), and some are dynamic route functions, replacing the parameters with the provided value (SET_PASSWORD({ passwordTokenId: 'pwt' })
returning /setPassword/pwt
).
We created a new router UnauthenticatedRoutes
, which matches the SIGN_IN
route with the SignIn
component, and redirects users to it in case they navigate to an unknown route.
We are also conditionally rendering different routers based on the user’s authentication status, which is extracted from useAuthStore
's auth
object.
We render:
AuthenticatedRoutes
– if auth
is setUnauthenticatedRoutes
– otherwiseWe implemented the SignUp
component to handle user registration. Below is a breakdown of its key features:
react-hook-form
with zod
for schema-based validation.useSignUpMutation
to send user details to the backend.preventToastErrors: true
.isLoading
state).react-i18next
to support multilingual text in both the form and confirmation message.AuthLink
component.ROUTES.SIGN_UP
in UnauthenticatedRoutes
.We implemented the ResetPassword
component to allow users to request a password reset. They can enter their email address to which we send out a link that allows them to securely set a new password. Below is a breakdown of its key features:
react-hook-form
with zod
for schema-based validation.useResetPasswordMutation
to send a reset password request to the backend.preventToastErrors: true
.isLoading
state).react-i18next
.AuthLink
component.ROUTES.RESET_PASSWORD
in UnauthenticatedRoutes
.As the next step in the authentication flow, we created the SetPassword
component, which allows users to set a new password using the passwordTokenId
from the URL path. Here’s how it works:
useVerifyPasswordTokenQuery
to validate the passwordTokenId
before displaying the form.OverlaySpinner
is shown to indicate loading.react-hook-form
with zod
for schema-based validation.passwordSchema
.useSetPasswordMutation
to send the new password and token to the backend.preventToastErrors: true
.useMessageStore
.IoEye
and IoEyeOff
icons.isLoading
state).react-i18next
for multilingual support.ROUTES.SET_PASSWORD({ passwordTokenId: ':passwordTokenId' })
where the passwordTokenId
is the parameter that will be replaced with the actual value when visiting /setPassword/pwt
(for example, pwt
).We also fixed a mistake where we previously defined VerifyPasswordToken
as a mutation
while it is a query
as it doesn’t have side effects.
The VerifyEmail
component handles email verification as part of the authentication flow. Below is a breakdown of its functionality:
verificationTokenId
from the URL parameters.useVerifyEmailMutation
on mount to verify the email with the backend.preventToastErrors: true
.passwordTokenId
, redirects the user to the Set password screen (ROUTES.SET_PASSWORD({ passwordTokenId })
).OverlaySpinner
) while verification is in progress.react-i18next
for multilingual support.With the authentication flow complete—allowing users to create, recover, and authenticate their accounts—we now need to provide a way for them to log out.
To avoid cluttering the main interface with a sign-out button, we implemented a hamburger menu in the navigation bar. This menu toggles a sidebar, which:
Pressing the Sign Out button triggers our previously defined logout
utility, which:
Logout
mutation, removing the refreshToken
from the database, invalidating it.apolloClient
cache.Sidebar
component with a floating layout.isSidebarOpen
state in useAppStore
to manage sidebar visibility.Signing out will trigger a sequence of events:
Logout
in the API will remove the refreshToken
from both the cookies and the database so it cannot be used anymore to generate accessToken
s.authStore
will make auth
be null
, so our Routes
component will switch routers from AuthenticatedRoutes
to UnauthenticatedRoutes
.UnauthenticatedRoutes
router will redirect the users to /signIn
.With these authentication flows in place, users can now sign up, reset passwords, verify their emails, and sign out securely. Our frontend is fully integrated with the GraphQL API, ensuring a smooth authentication experience.
A summary of all code changes can be found on this GitHub link.
Currently the users can authenticate themselves, but the tasks created don’t belong to their account as we are still using our local mock API for task operations. In the next post we will use Apollo Client to perform task operations with our real API.
Here's how our timeline look like now: