App that lets users record and rate their meals each day to help them stick to a diet.
π TRY THE APP LIVE Note: It's fine to use dummy data to sign in (it isn't used beyond authenticating users).
Expand Contents
I wanted to challenge myself by creating a non-trivial, full-stack project, from design to deployment, with a professional Git flow. I knew this would teach me more than multiple smaller projects because it requires more systems-level, architectural thinking as you work across different layers and domains.
- Friend wanted an app which kept track of what they were eating, but less strict than a calorie counter.
- User records basic information for each meal including a rating for healthiness, and at a glance you can see the daily average for all meals in a calendar.
- Further details can be seen in data visualizations.
- Sign up
- Click on a day in the calendar
- Choose mealtime and complete form
- Daily meal rating averages are understood via a visual gradient on each date.
- Statistics page presents data in various charts
- FAQs page subverted into much less useful Rarely Asked Questions
- Talked with friend about what they wanted.
- Sketched wireframes, then full designs (faster iterations than using Figma, for me)
- Modelled data for database, thinking about data flow throughout app while referencing designs, ensuring everything (as far as possible) was accounted for.
- Started coding. Spent large amount of time planning the project before writing any code. Led to more coherent structure, quicker development, more consistent design, less frustration.
- Often stepped away from laptop and thought about problem from a broader - and end user's - perspective.
- I decided not to use any tutorials (rarely do, anyway) and all ideas and implementations are my own. The struggling is the learning.
- GraphQL schema
- React component
- Auth in client
- Styled components
- Unit tests for util, custom hook, component
- Integration tests for API
- Dockerfile, Docker Compose
- GitHub Actions workflow
- Custom Eslint config
- Descriptive Pull Request
- Liberal use of comments
-
Development - I use Docker Compose to spin up React client, Express server, and MongoDB containers. Client and server containers are configured with bind mounts to local filesystem which allows for real-time updates as I develop. The Vite development server serves client files and proxies API requests to the backend.
-
Production - Project is hosted on the Fly.io PaaS. Whenever a PR is merged into the main branch a workflow is triggered which redeploys the app with the latest changes. User-uploaded images are stored in S3 and data is stored in MongoDB Atlas. The built client files are served by Express middleware from a static directory.
-
I used a monorepo structure with client and server subdirectories. This made deployment and CI simpler.
-
API is a GraphQL schema using Apollo Server.
-
Images are uploaded to S3 directly from the client using presigned URLs created on the server (reduces load on server).
- TypeScript, React, RadixUI, Styled Components, Vite, Apollo
- Jest, React Testing Library
- Express, MongoDB, Mongoose
- Docker, GitHub Actions, AWS SDK (S3, STS, IAM users/roles, presigned URLs)
- ESLint/Stylelint/Prettier
- GraphQL over a REST API as I wanted to challenge myself with a different way of thinking about querying data.
- Apollo Client because it handles data fetching, caching, and state management.
- RadixUI for quicker development, theming consistency, and ensuring accessibility.
- AWS S3 to store images because storing on the server filesystem would increase bandwidth and costs.
- Vite for client. Previously used a custom webpack config but found Vite to be much faster.
- Tried to simulate a real-world workflow to facilitate joining a team:
flowchart TD
A{{Choose task}}-->B{{Pull latest changes}}
B-->C{{Checkout new branch}}
C-->D{{Implement feature}}
D-->E{{Commit, run Git hooks}}
E-->F{{Push to remote}}
F-->G{{Open PR, run CI}}
G-->H{{Merge into always-deployable main}}
H-->I{{Run CD}}
I-->A
- Main branch protections (require PR, checks must pass).
- Small, frequent commits (around 90 PRs for project).
- Aimed for a daily commit which helped ensure small, manageable tasks.
- Descriptive PRs with clear commit messages and description of change.
- AWS root user is protected by MFA with no access keys, and IAM users have no privileges apart from assuming a temporary role attached to a policy with privileges only to upload to S3.
- Secrets/credentials stored in git-ignored .env files in development/testing environments, GitHub Action's Secrets engine during CI, and Fly.io's Secrets API in production.
- Passwords encrypted in database via bcrypt.
- git-leaks program runs in a Git hook and during CI checks which prevents AWS credentials from being committed accidently.
- JWT tokens used for user sessions have a short expiry in case they fall into hands of bad actors (like Steven Seagal).
- Git hooks via Husky runs checks before committing.
- Commitzen enforced to ensure standard Git commit messages.
- ESLint/Stylelint/Prettier, all with completely custom configs.
- Centralized GraphQL error handling where expected errors are waved through and unexpected ones are formatted to prevent internal details leaking.
- Lots of clarifying comments.
- Consistent directory structure and file naming.
- A cron job runs daily via GitHub Actions which checks health of deployed app at its base path.
- Developed mobile-first, responsive on tablet and desktop.
- Tested on Safari, Firefox, and Chrome for cross-browser compatibility.
- Frequent user testing - on different devices - to look for pain points.
- Redirect to referrer feature in client where unauthenticated users who access app at a restricted path will be redirected back to intended page after a successful sign-in.
- Initially created wireframes using Figma as with my FakeMates project but iterations were slow so I changed to (crudely annotated) hand sketched designs:
- Decided against an auth service like Okta/Auth0 in order to learn more about the process of authentication specifically in a GraphQL project, so hand rolled the entire auth system.
- GraphQL API protected by an expiring JWT token and graphql-shield library with its permissions and rules.
- Client is inherently unsecure since the user downloads the entire react bundle - hence the need for protections on API - but user session is handled with LocalStorage API, a JWT token, and global state.
- Manually tested during development of API with Postman and Apollo Server's testing playground.
- Unit testing of components, utils, and custom hooks. Basic smoke tests, user interactions.
- Comprehensive integration testing of GraphQL schema using Apollo Server's executeOperation API. Covers all resolvers and most edge cases.
- E2E tests not currently implemented because of time constraints. I would like to add Cypress tests to cover essential user flows like signup within a dedicated Docker Compose file which can spin up project in a testing environment and run in CI.
- Increased confidence in code and prevented regressions when adding new features.
- Helped with clarifying unclear topics and unfamiliar syntax (like this Markdown file with Mermaid syntax!).
- No code copied from its output is in codebase.
- Helpful for small routines, not larger features.
- Important to use as an aide and not rely on it as a black box solution - skills felt less sharp after deferring to it too frequently.
- Creating a full stack app from design to deployment is about 3% coding business logic, 90% configuration, and 7% mental anguish (the healthy kind).
- Third party tools are to be embraced.
- A lot of the work of a software developer can happen away from the computer.
- Importance of following the path of data and knowing what is happening where in the project.
- How to secure, test, refactor, deploy, and monitor an app.
- In general, whenever I faced a challenge I thought of the system as a whole and not just as lines of code.
- Codegen for creating types from GraphQL schema caused issues when running builds in Docker/CI. Considering what is available to the code in each environment helped.
- The backend had me doing mental gymnastics considering TypeScript, GraphQL, and Mongoose types all at the same time. Adding IDE extensions helped.
- Organizing react components into logical units. Aimed to limit files to under 200 SLOC.
- Add a monorepo framework such as NX to handle dependencies better.
- Implement as a native app which would give access to the camera and a simplified UX.
- Add a Cypress E2E test suite.
- Create reusable form fields.
- Finish image upload implementation in client; server-side resolvers and client form are all complete, just issue with rendering.
I have a greater appreciation of the entire SDLC. I feel more comfortable working across domains from user research and design to implementation and deployment. I enjoyed constantly learning and applying new tools and concepts throught the project and was aided by the thorough planning of data models/designs beforehand. I am keen to continue working with new technologies and refining my skills.
There was a specific focus on approaching this project in a way that facilitated joining a team and I feel I have achieved that as I am more comfortable working with Git branching strategies and PRs.
Thanks to my friends, my family, my partner, Kenco coffee, Tottenham Hotspur Football Club. You have all given me far more than I can ever repay. And to the maintainers of all the open source tools that I used, gracias.