Lessons Learned While Building a Full-Stack Flask + React App

GabrielleGabrielle
8 min read

I recently built a full-stack application called Via — a party and trip planning tool that helps users organize multi-day events with shared locations and scheduled activities. It’s designed for people who love planning — or are designated as the planner — and want one central space to coordinate events, destinations, and timelines with ease.

This project was built with a Flask + SQLAlchemy backend and a React frontend, using the Context API for global state management and Flask-Login for user authentication. The app includes full CRUD functionality, a many-to-many relationship between trips and places, and a user-friendly interface for managing events.

In this post, I’ll walk through some of the key technical decisions I made, the challenges I faced, and what I learned from building Via from the ground up.


Designing the Data Models

When I started planning Via, I knew the relationships between users, trips, places, and events would be central to the app’s structure. I needed a way to connect users to trips, and allow each trip to include multiple events and multiple places. After mapping out a few possibilities, I landed on four core models:

  • User: the person planning trips

  • Trip: a collection of events spanning a date range

  • Place: any destination, venue, or activity location

  • Event: a scheduled activity tied to a specific trip and place

To connect trips and places, I implemented a many-to-many relationship using an association table. This allowed each trip to include multiple places, and each place to be reused across multiple trips.

trip_place_association = db.Table('trip_place_association',
    db.Column('trip_id', db.Integer, db.ForeignKey('trips.id')),
    db.Column('place_id', db.Integer, db.ForeignKey('places.id'))
)

The Event model acts as the bridge between a Trip and a Place, storing information like the event’s name, location, and start/end time.

This setup gave me the flexibility to:

  • Query all events in a trip

  • See which places were associated with a trip

  • Reuse a place (like a hotel or venue) across different trips without duplicating data

Designing this schema upfront saved me time when it came to building out my routes and UI later.


CRUD + Routing

With my data models set up, I implemented full CRUD functionality using Flask-RESTful and Flask-Marshmallow for serialization. I created resource classes for each major model — Trip, Place, and Event — and followed RESTful conventions to keep my API predictable and frontend-friendly.

Instead of manually writing to_dict() methods, I used Marshmallow schemas to handle serialization. This helped me keep responses clean and consistent, and made it easier to return nested data — like a trip with its associated places and events — without writing extra logic.

On the frontend, I used fetch() to send data between React and Flask. When users created or updated data through forms, those actions triggered POST or PATCH requests to the API, and the UI updated accordingly. I also used useEffect to fetch initial data when components loaded.

Rather than relying on a standard GET /trips endpoint, I used a custom /check_session route that returns the currently logged-in user along with all their trips, each trip’s associated places, and any scheduled events. This allowed me to hydrate the app state immediately after login and avoid making multiple separate fetch calls on page load.

In addition to CRUD, I set up user authentication using Flask-Login. Users can sign up, log in, log out, and persist their session across refreshes. The /check_session route was key in keeping the frontend in sync with the backend and ensuring users could only access their own data.


Managing State in React

To keep track of the logged-in user and their related data (trips, places, and events), I used React’s Context API to create a global state that could be accessed across the entire app. This made it easier to avoid prop-drilling and allowed me to centralize session handling and state updates.

When the app loads, I trigger a checkSession() call in a useEffect hook inside my UserProvider. If the user is authenticated, the backend responds with their data, including nested trips, each trip’s associated places, and scheduled events. That data is stored in global state and passed down to any component that needs it.

useEffect(() => {
    checkSession();
}, []);

The structure of the data returned from /check_session looks something like this:

{
  "id": 1,
  "username": "gabrielle",
  "trips": [
    {
      "id": 2,
      "name": "Santa Barbara Road Trip",
      "start_date": "2025-07-18",
      "end_date": "2025-07-21",
      "places": [
        {
          "id": 11,
          "name": "Downtown SLO",
          "location": "San Luis Obispo, CA",
          "events": [
            {
              "id": 22,
              "title": "Lunch at Lincoln Deli",
              "planning_status": "confirmed",
              "address": "496 Broad St, San Luis Obispo, CA 93405",
              "start_time": "2025-07-18T12:00:00",
              "end_time": "2025-07-18T13:00:00"
            },
            {
              "id": 22,
              "title": "Afternoon Pick Me Up",
              "planning_status": "tentative",
              "location": "Scout Coffee Co. - 1130 Garden St, San Luis Obispo, CA 93401",
              "start_time": "2025-07-18T13:30:00",
              "end_time": "2025-07-18T14:00:00"
            }
          ]
        },
        {
          "id": 11,
          "name": "SB",
          "address": "Santa Barbara, CA",
          "events": [
            {
              "id": 22,
              "title": "Wedding",
              "planning_status": "confirmed",
              "location": "The Mission",
              "start_time": "2025-07-19T16:00:00",
              "end_time": "2025-07-19T23:00:00"
            }
          ]
        }
      ]
    }
  ]
}

This format allowed me to load everything the user needed in a single request, making state initialization simple and efficient.

The context also stores functions like signupUser, loginUser, and logoutUser, which handle the relevant fetch requests and update state accordingly. For example, after a successful login, the setUser and setUserTrips functions populate the context, allowing the dashboard to render user-specific data instantly.

I also created helper functions for adding, updating, or deleting individual trips, events, and places from state after each successful API call. This kept the frontend responsive and avoided unnecessary re-fetching.

Using Context for state management was a good balance between simplicity and control. Redux might be helpful in larger apps, but for Via, the Context API handled everything I needed while keeping the code lightweight and readable.


Challenges + Fixes

Like any full-stack project, building Via came with its share of roadblocks. Some were technical, others architectural — and a few were simply the result of overthinking a feature before just getting something working.

One early challenge was figuring out how to keep my frontend state synchronized with the backend after making changes. For example, when a user created a new event, I not only needed to update the events list, but also check whether a new place had been added to the trip (since places only appear if they have events). To solve this, I wrote helper functions in my context that could intelligently update the relevant pieces of state without needing to re-fetch the entire trip object.

Handling Many-to-Many Relationships

Another tricky part was building and maintaining the many-to-many relationship between trips and places. Since a place could exist in multiple trips, I had to be careful not to unintentionally remove or duplicate it when adding events. I used Marshmallow's nested serialization to control what was returned, and added logic on the backend to associate a place with a trip only when an event was created there.

Conditional Rendering Based on Planning Status

Because events could be either "tentative" or "confirmed", I added visual indicators and conditional formatting to help users distinguish between them in the UI. This added a layer of UX clarity — but also meant I had to keep the planning status logic consistent across forms, display components, and patch requests.

Debugging Session-Dependent Rendering

I also ran into a few bugs where components would try to access user data before the checkSession() fetch was complete. To fix this, I added a loading state in my context to delay rendering until all session data was available. This helped prevent unwanted crashes or empty component loads.

Each of these challenges pushed me to think more deeply about data flow, user experience, and how to make the frontend and backend work together smoothly.


Lessons + What I’d Do Differently

Building Via from scratch helped me grow not just as a developer, but as a planner and problem-solver. Working across the full stack forced me to think critically about how data is structured, accessed, and manipulated — and how to design features that feel intuitive to users but are also maintainable in code.

A few key lessons stood out:

  • Design your data relationships early. Getting the trip/place/event relationships right up front made the rest of the app much easier to build around.

  • Don’t over-fetch. Building the check_session route to preload just the data I needed was one of the best architecture decisions I made — it kept the app fast and reduced complexity.

  • Trust small reusable functions. Whether it was a backend helper or a frontend state update, abstracting repeated logic kept my code cleaner and easier to debug.

  • Done is better than perfect (at first). It was easy to get stuck trying to build the ideal solution, but iterating — even on the tricky parts — helped me make real progress.

There are still things I’d refine — like streamlining how I handle nested state or improving the overall user experience — but building this app gave me the chance to see an idea through from start to finish. It pushed me to solve real problems, think critically about structure and flow, and build features that actually work together. More than anything, it showed me how much I’ve grown as a developer and how much I enjoy creating tools that make people’s lives a little easier.


Conclusion

Building Via taught me what it really means to bring a full-stack application to life — from mapping out the data to structuring the API to making sure everything runs smoothly in the browser. Every piece of the process — even the frustrating ones — helped me build confidence in my technical skills and how I approach problem-solving.

I'm proud of what I created and excited to keep building — whether that means adding new features to Via or taking on new ideas entirely. This project reminded me that the best way to grow is to dive in, get stuck, figure it out, and keep going.

0
Subscribe to my newsletter

Read articles from Gabrielle directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Gabrielle
Gabrielle