
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:
useQueryuseLazyQueryuseMutationgraphql-codegenThis 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:successerrorwarninginfoid – 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_UPSIGN_INSET_PASSWORD({passwordTokenId})RESET_PASSWORDVERIFY_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 accessTokens.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: