Photo by Phil Hei

We Created a Tool to Plan Our Breakfasts

Breakfast with your friends and colleagues always involves a certain amount of organization. After a date has finally been found that works for everyone, there are still some things that need to be arranged:

  • Figure out who attends

  • Ask everyone what and how much they want to eat and drink

  • Communicate pricing to your guests

  • Collect money and check if everyone has paid

  • Write a shopping list

Typically, this is then organized in a WhatsApp group or in a Slack channel and quickly becomes confusing. These problems are solved by our app Weißwurst Planer, which supports its users in organizing meals together. The following describes how we have tried to implement the requirements for the project in such a way that it is as easy and data-saving to use as possible.

Requirements

It was important to us that the app is as simple and straightforward to use as possible, which is why we decided to use only the most necessary data from users for breakfast planning. As a template, we used the scheduling app Doodle, except that instead of scheduling it is about organizing breakfasts. For this purpose, we have considered the following requirements, which are also implemented by the app in this way:

  • There is an event host that can add an event. After creating the event, he receives an individual link that he can send to all guests, who can then add their orders.

  • Besides the organizational data (date, time, event name, description), the host can individualize the product range. For this purpose, he can either use the products suggested by us or add his own products with a price to the event. This allows users to plan other shared meals, such as dinner.

  • Guests can add their order under the event link. To make it as easy as possible for the guests, they only need their name (and no account). Nevertheless, they can still view, edit or delete their order at a later time, as the necessary data is stored in the guests' localStorage.

  • Unlike the guests, the event host has an account in the Weißwurst Planer app. This ensures that he can access and manage the data of his created events in any case.

  • The host can edit or delete all orders if a user loses access to his order.

  • After the order acceptance is closed by the host, he can view the shopping list. In addition, there is a checklist with all participants on which can be tracked who have already paid.

  • The app selects one person from all participants to purchase the order.

How We Used Amplify for the Backend of Our App

The requirements for the backend consist of the connection to the database, user authentication, and the implementation of the business logic. To keep the effort and development time of the project as low as possible, we decided to use backend as a service. We implemented this with AWS Amplify, which generates most of the code needed to communicate with the backend (e.g. GraphQL queries) automatically. Besides Amplify, there are also alternatives, such as Firebase. Amplify is considered a bit more flexible compared to Firebase and also offers a GraphQL API in addition to a REST API. The following describes how we used the various AWS services to implement our project with some code examples.

User Authentication with Cognito

Amplify uses Amazon Cognito for user authentication. In addition to the login via the email address or the Amazon account, logins via Apple and Google are also accepted. For our project, we only use the login methods email and Google Account. For this, we first added auth to our app via the Amplify CLI using the amplify add auth command. For authentication handling in the frontend, Amplify offers two options: You can use the pre-built UI components or call the authentication APIs manually. In our project, we use both options.

For log-in and sign-up handling, we use pre-built UI components from Amplify. For this, Amplify provides a withAuthenticator higher-order component (HoC) in React projects, which wraps the corresponding components. This ensures that when the component wrapped with the HoC is mounted, the log-in/sign-up screen is shown if the user is not logged in. In addition, the withAuthenticator HoC passes the props user and signOut to the wrapped component.

To access the user's data in other components we use the API provided by the Auth class. This provides over 30 methods, such as changePassword or signOut. For easier access to the methods relevant to us, we have created a custom React hook:

import { useEffect, useState } from 'react';
import { Auth } from 'aws-amplify';
import { User } from '../../models';
import { useNavigate } from 'react-router-dom';

export interface LoginStatus {
  loginStatusLoaded: boolean;
  user: User | null;
  isLoggedIn: boolean;
}

export const useCognitoLoginStatus = () => {
  const [user, setUser] = useState<User | null>(null);
  const [loginStatusLoaded, setLoginStatusLoaded] = useState(false);
	const navigate = useNavigate();

  useEffect(() => {
    Auth.currentUserInfo().then((userData) => {
      userData
        ? setUser({
            mail: userData?.attributes?.email,
            id: userData?.id,
            name: userData?.username,
          })
        : setUser(null);
      setLoginStatusLoaded(true);
    });
  }, []);

  const logOut = () => {
    Auth.signOut().then(() => {
      setUser(null);
      setLoginStatusLoaded(false);
      navigate('/');
    });
  };

  return { loginStatusLoaded, user, isLoggedIn: Boolean(user), logOut };
};
use-cognito-login-status.tsx

In addition, the current login state is stored in the React context to avoid waiting time to access the API.

Data Management with AWS Lambda, AppSync and DynamoDB

To connect an API and a database to the app, the Amplify CLI provides the amplify add api command. After configuring the API in the CLI, the GraphQL schema is autogenerated in the app. This schema can then be edited and deployed using the amplify push command. Alternatively, one can use the graphical editor in the Amplify Studio web interface for this purpose. In addition to the GraphQL schema, various mutations, subscriptions, and queries are also generated.

To access the API in the frontend, the API class of aws-amplify and the GraphQL operations (e.g. getEventsOfAdmin from the autogenerated ../../graphql/queries folder) must be imported. Then the graphql method of the API class can be called with the corresponding query and input parameters. The following example shows the fetching of all events of an event host with its unique ID (= adminId):

//...
import { API } from 'aws-amplify';
import { getEventsOfAdmin } from '../../graphql/queries';

const fetchEvents = async () => {
	//...
    const eventData = await API.graphql({
      query: getEventsOfAdmin,
      variables: { adminId: user.id },
    });
	//...
}
admin-events-context.tsx

Of course, the autogenerated GraphQL queries, subscriptions, and mutations cannot cover all use cases. Therefore we added lambda functions to our project. This works with the Amplify CLI command amplify add function. After that, you can customize the properties of the lambda function, like the name or the access rights to the API. To access the lambda function in the frontend, it must be added to the GraphQL schema under the appropriate type (Query, Mutation or Subscription) using the @function directive. This looks like this in the example of the getEventsOfAdmin query used above:

type Query {
  getEventsOfAdmin(adminId: ID!): [Event] @function(name: "getEventsOfAdmin-${env}")
  # ...
}
schema.graphl

Inside the lambda, we can then also (if configured when adding the lambda) use the AppSync GraphQL API. Within the lambda, we can then access the GraphQL endpoint with process.env.API_<YOUR_API_NAME>_GRAPHQLAPIENDPOINTOUTPUT and the API key with process.env.API_<YOUR_API_NAME>_GRAPHQLAPIKEYOUTPUT. This looks like this in the example of our getEventsOfAdmin lambda:

import { default as fetch, Request } from 'node-fetch';

const GRAPHQL_ENDPOINT =
  process.env.API_WEISSWURSTPLANER_GRAPHQLAPIENDPOINTOUTPUT;
const GRAPHQL_API_KEY = process.env.API_WEISSWURSTPLANER_GRAPHQLAPIKEYOUTPUT;

const getEventQuery = /* GraphQL */ `
  query LIST_EVENTS(
    $filter: ModelEventFilterInput
    $limit: Int
    $nextToken: String
  ) {
    listEvents(filter: $filter, limit: $limit, nextToken: $nextToken) {
      items {
        id
        name
        description
        # ...
    }
  }
`;

/**
 * @type {import('@types/aws-lambda').APIGatewayProxyHandler}
 */
export const handler = async (event, context, callback) => {
  const adminId = event?.arguments?.adminId;

  /** @type {import('node-fetch').RequestInit} */
  const options = {
    method: 'POST',
    headers: {
      'x-api-key': GRAPHQL_API_KEY,
    },
  };

  const getEventsRequest = new Request(GRAPHQL_ENDPOINT, {
    ...options,
    body: JSON.stringify({ query: getEventQuery }),
  });

  let requestedEvents;
  try {
    if (!adminId) {
      throw new Error('adminId is undefined');
    }

    const getEventsResponse = await fetch(getEventsRequest);
    const getEventsBody = await getEventsResponse.json();
    requestedEvents = getEventsBody.data.listEvents?.items;

		//business logic
		//...
  } catch (error) {
    callback(error.message, null);
  }
  callback(null, requestedEvents);
};
getEventsOfAdmin.js

Amplify also provides the ability to mock and test changes locally. This is possible for the API, the storage, and the lambda functions. This makes it very easy to test and debug changes locally. The lambda described above could then be tested with the command amplify mock function getEventsOfAdmin for example. The input parameters must be added to a JSON file, which is generated by default when creating a new lambda inside the src folder.

How We Made the Frontend

If you look at the package.json you will notice that besides the dependencies directly related to React or Amplify there are only the following entries:

  • styled-components: Helps to style the project at the component level with a mixture of JavaScript and CSS

  • date-fns: Provides helpful functions to simplify the handling of dates in JavaScript

  • react-router-dom: Provides client-side handling of the routing

  • react-hook-form: Helps to deal with forms in JavaScript. This helped us with the implementation of the CreateEventForm.

  • react-helmet: Enables dynamic modification of the document head

Everything else we implemented ourselves. This also applies to the state and context management for which we used the React Context API.

Conclusion

To sum up, we could realize all the predefined requirements with Amplify's help. In doing so, most of the services used by Amplify could be configured either directly in the CLI or in Amplify Studio. Everything else we were able to set up fairly easily on the individual services' pages in the AWS console. In addition, there is now also a relatively large community that can usually answer any questions about Amplify related to smaller projects. We would be happy if you remember our app the next time you plan a breakfast with your friends and colleagues and we can help you organize it.

Interested in working with and for us?

Career