Building an App at the Midpoint of Launch School Core

Using Fundamentals to Bring an Idea to Life

Jason Wang
27 min readMay 24, 2022

TL;DR I built an app after RB185 and used valuable lessons from every Core course to implement features and solve problems.

Launch School Core can be a long journey and significant commitment. You need conviction in the value of the curriculum and the discipline to see it through. In the earlier courses, it’s difficult to understand how the material relates to real-world applications. Is it worth spending hundreds of hours learning syntax and solving logic puzzles? How will that help me make a beautiful interactive website? Or as another student phrased it: “When can I make something real?”

Before Launch School, I had ideas for apps but not the skills to bring them to life. Now that I’m halfway through Core, I’d like to share how the curriculum helped me design and build a small web app. This is not a tutorial, but rather a chronicle of challenges I faced, and how knowing fundamentals enabled me to find solutions. I try to provide specific examples of lessons learned from each course.

If you’re in the first half of Core, maybe this project and others in the Show and Tell forum can give you an idea of what’s possible at this stage. I would be delighted if reading this article sheds some light on what to expect in later courses and motivates you to keep moving forward. Please feel free to reach out with comments, corrections, and especially if you’ve figured out a way to break the app.

The Application: Eight Wonders

Eight Wonders is a flight path optimizer which finds the shortest path connecting a series of airports. Imagine you’re on a world tour, flying around the globe. This tool helps you minimize time spent on the plane so you can have more time exploring the destinations. I made the app because I’ve always dreamed about taking a round-the-world trip someday. I know there are travel planning considerations more important than distance (such as event dates), but as an engineer I‘m compelled to find the most efficient path.

Why eight? The idea is that you can visit the Seven Wonders of the World, plus an Eighth Wonder (whatever you decide that to be). There are a few other reasons:

  • For the user interface, I wanted to set a reasonable upper limit so that there is a sense of completion (with visual indicators) once all 8 destinations have been entered.
  • For large n, the sorting computation takes too long.
  • 8 is a lucky number in my culture 🧧.

Features of the App

  • Create an itinerary. Start from a blank slate or customize a pre-made one that already has its destinations populated.
  • Retrieve an itinerary by entering its unique 8-character code.
  • Edit an itinerary’s name.
  • Delete or Copy an itinerary.
  • Share an itinerary by generating a small image you can screenshot.
  • Add or Delete destinations from an itinerary. Each destination is linked to an airport code (e.g. LAX for Los Angeles Intl Airport). An itinerary can have up to 8 destinations.
  • If you have less than 8, the app can randomly select the remaining destinations from a pool of 8,800+ airports. This is my favorite feature because I always receive interesting destinations that I’ve never heard of before. Today I got the island of Izu Ōshima, Japan, known for its active volcano and camellia flowers.
  • Add or Delete experiences from a destination. A destination can have up to 3 experiences.
  • View your itinerary on a world map at GC Map.
  • Click on an airport name to view it on Google Maps.
Itinerary Management Page

The Value of Core

Each Core course unlocked skills useful for building the app:

RB101 focuses on Ruby fluency and solving problems in a structured manner. Familiarity with Ruby syntax enabled me to quickly express my app ideas as code. I used PEDAC to solve the problem of finding the shortest path.

RB120 introduces us to object-oriented programming (OOP) with classes, modules, and objects. To organize my app, I created classes for an Itinerary that contains Destinations, which contain Experiences.

RB130 covers closures, testing, and packaging code into a project. I used blocks to defer the sorting implementation to method invocation time. I wrote tests to prevent regression after adding new features. Familiarity with Gemfiles helped me fix a deployment issue. Regex also came in handy.

LS170 delves into how the Internet works, with a discussion of network infrastructure and protocols. Understanding the HTTP request/response cycle and URL structure was invaluable when debugging.

RB175 is where fundamental concepts from previous lessons are integrated to build projects. My app is functionally similar to the Todo List project, in which we Create, Read, Update, and Delete (CRUD) data. For example, we can Read an itinerary, Update its name, Create a destination to add, and Delete an experience.

LS180 focuses on structuring and interacting with relational databases, using SQL. Normalizing my data helped avoid duplication. Carefully designing table relationships helped me enforce data integrity and write efficient queries. Fluency with SQL made it easy to troubleshoot issues and modify data.

RB185 builds upon RB175 by transitioning our Todo List project’s data store from a session to a database, enabling long-term storage. With these skills I was able to write methods for safely querying a database for itinerary data.

LS202 teaches HTML so we can structure our content, and CSS to style its appearance. I used these to give the app a responsive design.

In the following sections, I detail various challenges and explorations that I found interesting.

RB101: Using PEDAC to Find the Shortest Path

The main idea of the app is that it will automatically sort the airports in your itinerary. This sorting method is invoked every time the Itinerary Page is loaded. Therefore, it was critical to solve this problem first.

Problem Statement

Given the coordinates of 8 airports, find the shortest flight path that starts from the first airport, travels to the other 7 (without revisiting any), and returns home. A coordinate is a pair of numbers: (latitude, longitude).

Input: An array of arrays. Each sub-array contains two floats representing airport coordinates: [latitude, longitude].

  • Example input: [[33.942, -118.407], [55.617, 12.656], ... ]

Output: A hash representing an index mapping. The keys are the original array indices, and the values are the new sequential indices (0 through 7).

  • Example output: { 0 => 0, 4 => 1, ... }. The algorithm determined that the airport originally 5th in the list (index 4) should actually be visited 2nd (index 1).

Requirements

  • The first airport in the array will always be the starting point.
  • The array will contain 1 to 8 airports. There is no need to sort if we only have 1 or 2 airports.
  • It’s possible but rare for multiple airports to have the same longitude value. In this case, those airports can be ordered in any way.
  • It doesn’t matter if we travel eastward or westward around the world.

Clarification of Problem Domain

In RB109 we are encouraged to ask for clarification on unfamiliar terms. For this problem I needed to define longitude and latitude.

Latitude represents the position north or south of the equator. Longitude is the position east or west of the Prime Meridian, a vertical line running from the North Pole to South Pole, passing through Greenwich, England. You may have heard of Greenwich Mean Time, as longitudes are used to define time zones. Starting at the Prime Meridian, longitude values range from 0 to +180 degrees traveling eastward, and 0 to -180 traveling westward. On the opposite side of the globe from Greenwich is the antimeridian, where the +180 and -180 lines meet. Together, the Prime Meridian and antimeridian form a great circle which splits the Earth into the East and West hemispheres.

Examples and Test Cases

The following test case shows an incorrectly-ordered array, the correctly-ordered array, and the index mapping hash used to reorder the first array to the second.

Test Cases

Data Structure

We will use arrays and hashes to take advantage of Ruby collection methods such as #map and #sort_by.

Algorithm

We can find an approximate solution by sorting the airports by their longitude so that we are always traveling east (or west). In the following diagram, we start from LAX and want to visit 3 other airports. Visually we can see that the correct path is LAX-JFK-CPH-HKG-LAX (or the reverse).

Los Angeles (LAX) and New York (JFK) in the Western Hemisphere have negative longitude values, while Hong Kong (HKG) and Copenhagen(CPH) in the Eastern Hemisphere have positive values.

Algorithm Steps

  1. Normalize all values so they are in the range [0, 360], by adding 360 to negative values. This idea came directly from the the After Midnight Small Problem where we normalized the minutes in a day. At this point we can’t sort in ascending order because LAX is not the smallest value.
  2. Subtract LAX’s value of 242 from all values, so LAX becomes the zero point. Now LAX is the smallest, but HKG and CPH have become negative.
  3. Normalize again by adding 360 to negative values. Now we can sort in ascending order by longitude.
Sort Longitudes Algorithm, Illustrated
Sort Longitudes Algorithm

Algorithm Inaccuracies

The algorithm always moves in one direction (east or west). However, in some cases it’s shorter to travel in the reverse direction for specific segments. In the following example, the algorithm travels from Seattle (SEA) down to Los Angeles before moving up to Boise (BOI), because BOI is east of LAX. It would actually be shorter to go to Boise first. This is less of a problem if the airports are far apart.

Further Exploration

With some additional research I realized that this was a version of the Traveling Salesman Problem, which asks:

“Given a list of cities and the distances between each pair of cities, what is the shortest possible route that visits each city exactly once and returns to the origin city?”

Solving this would yield an exact solution without the inaccuracies of my first algorithm. We can solve it using brute-force, which involves finding every possible travel path:

  1. Reserve the first airport as a starting point.
  2. Take the remaining airports and find all possible permutations (Array#permutation is useful here).
  3. Add the first airport to both the beginning and end of each permutation, since we depart from and arrive back home.
  4. Compute the total travel distances, and select the shortest path.

Ultimately I decided to implement a dynamic programming solution which is faster than brute force, but that’s outside the scope of this article. Take a look at the Github repository if you’re interested in the three algorithms: 1. Sort by longitude, 2. Naive (brute-force), 3. Held-Karp DP.

Summary

RB101 is the foundational course because we must fully internalize its basic concepts before advancing to more complex ones. As a direct result of rigorous assessments and practicing with peers in RB101/109, writing the Ruby code for this app went smoothly. If I forgot specific syntax, such as whether #sort_by! is in Enumerable or just Array, I could quickly look up the docs thanks to our training in RB101.

The particular problem of finding the shortest travel path isn’t directly related to the string and array manipulation problems in RB109, but the PEDAC process was just as useful. It helped me organize my thoughts and develop solutions in a structured way.

RB120: Using OOP to Organize My Code

Using CRC Cards to Model Class Relationships

OOP helps us develop complex programs by organizing code into self-contained pieces that interact with each other. We create classes to encapsulate data and functionality, and define a public interface to provide access to their variables and methods. During the brainstorming process it can be helpful to identify the nouns and verbs, specify behaviors, and create Class Responsibility Collaborator (CRC) cards.

The logic of Eight Wonders is much simpler than the games we write in RB120, but creating CRCs was still useful for modeling interactions among classes and modules. An Itinerary object can store Destination objects (as collaborator objects) in its state, due to their associative “has-a” relationship. In order to fulfill its responsibilities, an Itinerary can invoke the public getters of its Destinations to access city and airport information.

Class Responsibility Collaborator (CRC) Cards

Using Inheritance to Share General Behaviors

A module is a collection of behaviors that can be mixed in with other classes via interface inheritance. We consolidate behavior in a module so it can be reused. The Eight Wonders app connects to, disconnects from, and queries a database, so I created module DatabaseConnection with instance methods for those actions. Then I created class ItineraryHandler which mixes in DatabaseConnection to gain access to those database-related methods. ItineraryHandler also contains behavior specific to itineraries, such as retrieving and deleting them from the database.

When thinking about how to structure relationships, to me it made the most sense that ItineraryHandler “has-an” ability to connect to a database, so I used interface inheritance. In the future, if the app was expanded with hotel reservation functionality, I could create class ReservationHandler and have it also mixin DatabaseConnection to reuse that code.

It would also be valid to say that ItineraryHandler “is-a” connection to a database since all of its instance methods are database-related. We could implement that by converting DatabaseConnection to a class and using class inheritance: class ItineraryHandler < DatabaseConnection. An example of that is in another student’s project.

Using Encapsulation to Protect Functionality

After ItineraryHandler queries the database, it must process the query result into the format expected by the rest of the app. These data processing methods are implementation details encapsulated inside ItineraryHandler because they are of no concern to any code outside the class. Therefore I wrote them as private instance methods because method access control limits undesired access to functionality. Since they are not part of the public interface, they would not appear on a CRC card.

Summary

OOP offers us an intuitive way to structure our code, by modeling objects after real-world entities with attributes and behaviors. Instead of storing data in hashes and arrays, I used OOP principles to structure my code into Itinerary, Destination, and Experience classes. Creating a DatabaseConnection module enabled me to extract generic behaviors to a single place and avoid repeating code. As I move on to larger apps, I will need to be thoughtful about structuring code in a sensical and coherent way.

RB130: Closures, Testing, and Tools

Using Blocks to Defer Implementation

RB130 goes into detail about the closures that we’ve using since RB101, with a focus on blocks. A closure is a “chunk of code” that can be saved, passed around, and executed later. It binds (retains references to) in-scope artifacts in its surrounding environment/context, building an enclosure around them. A closure carries and has access to its binding wherever it goes.

One use case of blocks is to defer implementation to method invocation time. The method implementor defers implementation details to the method caller, who decides at method invocation time which refinements to make to the implementation details. This enables us to write a generic method that is more flexible than hard-coding logic for specific scenarios. For example, Array#each is a generic method to which we can pass a block containing specific instructions to be carried out using each of the collection elements.

My Itinerary#sort_destinations! instance method re-orders the destinations using one of the three shortest path algorithms we discussed earlier. When I first wrote it I used a case expression to select an algorithm based on a flag argument (Symbol). After additional research I found many other algorithms for the Traveling Salesman Problem including nearest neighbor, genetic algorithm, and 2-opt. If I wanted to implement these in the future, I would have to update the case expression each time to accommodate the new flags.

To make sort_algorithms! more flexible, I refactored it to yield execution to an implicitly-passed block. To avoid a LocalJumpError exception, I used Kernel#block_given? to check if yield would execute a block in the current context. If not, then the method returns early. On the side of the method caller, I refactored the method invocation to pass a block which takes as a parameter the airport coordinates to be sorted.

Deferring Method Implementation, with Blocks

Using Testing to Prevent Regression

One reason to test software is to prevent regression. When adding features or refactoring our code, we want to ensure that these modifications are not introducing new bugs. In RB130 we practice the SEAT algorithm: Set up, Execute code, Assert results, and Tear down. We use the Minitest library which enables us to write assert-style tests using ordinary Ruby code.

In the context of a web app, tests may involve performing a GET or POST request and verifying that the response status code or body equals or contains an expected value. In the following example I verify that if a user enters an invalid airport code, input validation occurs, and the appropriate error message is displayed.

Testing User Input Validation

I periodically ran tests (with rake) while developing this app, and several times I was alerted to new bugs introduced after adding features. For example, when I added input validation for the airport codes, the test report revealed that I had forgotten to update the error message. I think my tests could be much more comprehensive, and I need more practice in that area. Tests are a critical part of the development process, and I look forward to learning about other types such as unit and integration testing.

Core Ruby Tools

RB130 shows us how to use Ruby Version Managers to manage Ruby installations, the Bundler gem to manage gem dependencies, and the Rake gem to automate development tasks. Using the Core Ruby Tools book as a reference, I had no issues installing Ruby 3.1.2 and various additional gems, and creating a basic Rakefile to run tests.

I’m working on an M1 Mac, and my first deployment to Heroku failed with a message indicating that my bundle only supported the arm64-darwin-21 platform (Apple Silicon) and not x86_64-linux. It instructed me to execute bundle lock —-add-platform x86_64-linux which I suspected had to do with Gemfile.lock. The Bundler documentation mentioned that the bundle lock command enables the bundle to support platforms other than my local platform. I examined Gemfile.lock and confirmed that only arm64-darwin-21 was listed under PLATFORMS. I executed the provided bundle lock command, verified the addition of x86_64-linux to Gemfile.lock, committed the change to git, and then successfully performed the deployment. I experienced this error again when deploying to fly.io and was able to to fix it the same way. Without the lessons from RB130 I might have just blindly executed the command and moved on without understanding the fundamental issue.

Using Regex for Input Validation

In RB130 we are advised to read the Regex Book because it is useful for a variety of string-related tasks. Don’t skip learning regex; I promise it’s fun!

When an Eight Wonders itinerary is created or copied, a unique 8-character itinerary code is created using Nano ID and assigned to that itinerary. All user-created itineraries on Eight Wonders are publicly accessible by anyone with the itinerary’s code. This is terrible for data security, but I chose to omit password protection in favor of lower-friction collaborative editing. Nano ID uses the alphabet A-Za-z0–9_-, so I wrote a pattern to validate input when a user retrieves an itinerary. The character class \w includes A-Za-z0–9_, so I just needed to add -, escaped with \ because - is a meta-character inside a character class.

Regex for Input Validation

Summary

RB130 covers a variety of topics, all of which are interesting and valuable in their own way. I still have a hard time intentionally using explicit blocks and procs in my own code, but I can understand what is going on when I see it in others’ code. Writing tests can be tedious, and to be honest I procrastinated writing them until halfway through the project. I’ll need to be more diligent in this area because testing is an essential part of writing resilient code. Lastly, knowing how to use core Ruby tools will become increasingly important as we work on larger apps with more complex dependencies.

LS170: Understanding How the Internet Works

LS170 departs from our Ruby path and ventures into how the Internet works, with a discussion of network infrastructure and protocols. It goes into detail about the protocol layers of computer network communication. The Application Layer is particularly relevant when developing web apps because we rely on an understanding of HTTP to route, process, and respond to client requests.

Using an API Tool to Find and Fix Vulnerabilities

During development I frequently used the Postman tool to test that my routes were operating as expected. For example I could send a POST request to add an experience to a destination; this simulates a user submitting a form. I would then examine the response body to verify that it included the correct flash message text and the text description of the new experience.

Using Postman to Send an HTTP POST Request

Eight Wonders has six pre-made itineraries (shown on the home page) that are not intended to be modified. When you click one of their Customize buttons, a copy is made so you don’t edit the original. There are no HTML forms in the user interface that would let the user send a destructive POST request for these six. However, I realized that if you were familiar with the app’s route patterns and knew the itinerary code, you could easily create a URL to access those six itineraries: .../itinerary/immutable_itinerary_code. Even worse, you could use an API tool to send POST requests to routes that delete or edit data for these supposedly immutable itineraries.

To fix this, I added an editable attribute in my itineraries database table. It’s set as false for the six itineraries when seeding the database, and defaults to true for other itineraries. I then updated my route blocks to query the database to check if an itinerary is editable, before performing any destructive action. There may be a better way to accomplish this using a before filter.

This refactoring increased request latency because an additional query is made for the validation. To mitigate this I wrote method find_itinerary_info which only returns high-level itinerary metadata including the editable boolean. This is in contrast to the full find_itinerary method which uses multiple JOINs to load related information from other tables. Those data are superfluous for checking if an itinerary is editable.

Using Wireshark to Examine TCP and TLS Handshakes

To communicate securely over the insecure plaintext HTTP protocol, we use HTTPS which encrypts requests and responses using the TLS cryptographic protocol. Encryption is used to encode messages so they can only be read using an authorized decoding mechanism (a key). TLS specifies cipher suites, which are collections of algorithms:

  • Asymmetric key exchange algorithm for the initial TLS handshake (assumed to be ECDHE for TLS 1.3).
  • Symmetric encryption/decryption algorithm (e.g. AES_128_GCM) for the bulk of the website traffic, i.e. HTTP requests/responses.
  • Digital signature algorithm (e.g. RSA, ECDSA) for authentication of certificates.
  • Hashing algorithm (e.g. SHA-256) for ensuring that the data hasn’t been altered on the way from sender to receiver (data integrity).

I was interested in examining the process of establishing a secure communication channel, so I used Wireshark to inspect a series of packets while visiting the Eight Wonders home page. Fellow students recommended this Wireshark video, and I also referenced this sslstore article. Here is a screenshot of Wireshark with my annotations regarding the process.

Using Wireshark to Inspect Packets

RB175: Our First Web Apps

This course enhances our understanding of HTTP with an introduction to servers and frameworks. We start by building a simple application server which manually processes HTTP requests. Then we move up one level of abstraction by placing the Rack interface between the server and our application code. Next, we use the web development framework Sinatra which makes it easier to write Rack-compliant application code. Finally we deploy our Todo List app to the Heroku platform so the world can access it. Along the way, we learn to write tests to verify that our routes are working as intended.

Eight Wonders is a simple CRUD app, and its structure is based on the Todo List app. I have app.rb which defines the get and post routes, several .rb files containing business logic, view templates for displaying data, static assets, and various configuration files.

Rendering Partial View Templates (Nested Templates)

In ERB view templates, we use tags to execute Ruby code and convert the evaluated result to HTML. This enables us to dynamically generate HTML web pages. A layout is a view template that wraps other view templates. Since the navigation bar and footer of Eight Wonders is the same for every page, I placed those in layout.erb. In the middle there is a <%== yield => tag that renders the Home, Itinerary, Sharing, or FAQ page depending on the route.

We can break up view templates into smaller chunks to better organize and reuse our code; these are called partial templates (partials). Since each itinerary can have 8 destinations, I created a destination_card.erb partial which I wanted to use up to 8 times in the itinerary.erb view template. I rendered the Itinerary page by invoking erb :itinerary, layout: :layout in the route block, but wasn’t sure how to do that again from within a view template.

After looking everywhere except the official Sinatra documentation, I found the answer in the official documentation. It turns out we can simply invoke erb from within a tag in itinerary.erb. We can use the locals option to pass local variables to the partial, such as information for a single destination.

ERB Partial Template

Using the Session to Simulate State

HTTP is a stateless protocol, which means each request/response cycle is independent. If a web app only used HTTP, it wouldn’t be able to tell if the requests it receives are related to each other. I wouldn’t be able to stay logged in to a website because it wouldn’t remember my login status when I navigated to another page. To provide a better user experience, we can persist state across multiple HTTP requests using techniques such as sessions. Session information is stored in a cookie file which we send along with our HTTP requests so the server can identify us and serve relevant data.

In the Todo List project, we use Sinatra’s :sessions feature to store an array of lists, and within each of them, an array of todo items. We also store temporary flash messages, which are displayed once and then deleted. Later on in RB185, we use the adapter pattern to create a consistent API that gives us the flexibility to use either the session or a database as a data store. Eight Wonders was written to use a database for long-term storage, and I did not use the session very much. If I added a feature where you could add plane tickets to a shopping cart and checkout, using a session might be appropriate.

In Chrome DevTools, we can see that Sinatra is using Rack’s session management implementation (Rack::Session::Cookie). We can also see that the cookie the server provided to us is included in the header of any HTTP requests we send. If we click the “x” button to delete the cookie, the next time we refresh the page we will receive a new cookie with a different value.

Viewing Cookies and Request Headers in DevTools

Using Environment Variables to Keep Sessions Secret

When I send the server a cookie to identify myself, the server does not know if my cookie is legitimate or if it has been tampered with. Therefore, when the server first provides me the cookie, it should encrypt it and attach its signature. Then, whenever I send the cookie, the server can verify that it is indeed me. To accomplish this, Rack first encodes the session data. Then it passes the encoded data and the session secret to the HMAC-SHA1 hashing algorithm to create a hash value; this signature is appended to the end of the cookie. Only someone with the session secret would be able to tamper with my cookie.

The session secret should not be a trivial string such as "secret”. Instead, we should use a secure random number generator to create a secret that is at least 32 bytes. We can store this in a local read-only .env file for development purposes, and add it to .gitignore (usually). Use the gem dotenv to load environment variables from .env to ENV in development, so we can reference ENV['SESSION_SECRET'] in our Sinatra app.

Creating a Secure Session Secret

Since .env is for production, for deployment we’ll need to configure environment variables via the CLI, or on the platform dashboard:

Summary

I particularly enjoyed RB175 because it was a practical application of our accumulated skills in order to solve a real problem: managing tasks we need To Do. This process helped me solidify the concepts from LS170, and it was gratifying to finally interact with my work in a live app. The course also revealed how some of the “plumbing” behind an app works. Specifically, it showed how what we type in or click on in a browser will trigger routes to execute logic and return content for display.

LS180 & RB185: Learning About Databases and Incorporating One Into the App

LS180 introduces us to the relational model for databases, and SQL for interacting with them. A relational database persists data in relations (usually tables), and the database structure is defined by the schema. Normalization is the process of designing schema to avoid anomalies. It involves splitting data into multiple tables and defining relationships among them, to remove duplication and improve data integrity.

Designing the Database Schema

Eight Wonders has a simple database schema:

  • One itinerary can have Many destinations, or none at all.
  • At One destination, we may want to partake in Many experiences, or none at all.
  • One airport can be referenced by Many destinations.
  • Each destination must have a relationship with an airport and an itinerary.
  • A consequence of this design is that destinations serves as a cross-reference table so that there is a Many-to-Many relationship between airports and itineraries.
  • I applied the ON DELETE CASCADE to all three foreign keys so that if a referenced record is deleted, any rows referencing it are automatically deleted. For example, if an itinerary is deleted, the destinations referencing it will also be deleted. Cascading down, any experiences referencing those destinations will be deleted as well.
Entity-Relationship Diagram (dbdiagram.io, with edits)

Optimizing a Database Query

RB185 covers how to use the pg gem so we can write Ruby code to interact with a PostgreSQL database. We learn how to perform queries safely using PG::Connection#exec_params and process the tuples in the returned query result. After changing our Todo List app’s data storage functionality from session persistence to database persistence, we touch on optimizing queries.

When the Eight Wonders Itinerary page is loaded, the app must retrieve the itinerary and all its associated data, from multiple database tables. I initially wrote multiple SELECT queries to perform this because the data was easier to process in Ruby afterward. However, this was inefficient due to the N+1 query problem. I was making 17 queries in total!

  • 1 query for itinerary info
  • 8 queries for destination info
  • 8 queries for each list of experiences

To improve performance, I consolidated all of this into a single query. The tradeoff was needing to write more complex logic in Ruby to process the query result.

SQL Query for Itinerary Data

Migrating Platforms to Save on Hosting Costs

The day after deploying my app to Heroku, I received this email:

[Warning] Approaching row limit for hobby-dev database…

The database contains 8,951 rows. The Hobby-dev plan allows a maximum of 10,000 rows. If the database exceeds 10,000 rows then INSERT privileges will be revoked…

To avoid a disruption to your service, migrate the database to a Basic ($9/month) or higher plan…

Eight Wonders has 8,802 airports in its database. After adding some seed data the total number of rows approaches 9,000. Each itinerary can have a maximum of: 1 itinerary + 8 destinations + (8 destinations * 3 experiences) = 33 rows. This means that users can only add a combined 30–40 itineraries before no more data can be inserted. If I upgraded to the Hobby Basic plan I would have 10 million rows, but most of that space wouldn’t be used. Mostly, I just didn’t want to pay $9/month; I can buy two oat milk lattes ☕ with that.

I read that fly.io was offering “free” Postgres for small projects, up to 3GB, which was plenty for my data size of only 10 MB. Using the excellent fly.io docs, I was able to create a project, attach and seed my Postgres database, and be up and running later that morning. I also created a Github Action to automatically deploy my app after executing git push. Since we have all been working with the command line and git since LS95, I was comfortable following fly.io’s configuration instructions.

Summary

I highly recommend building your own app after RB175 or RB185. Not as “assessment-driven development” to prepare for RB189, but as a personal confidence booster. Until the end of RB185 I often felt I was on training wheels, following video tutorials with brief instances of implementing features on my own. In the early phases of developing Eight Wonders, I sometimes had a sense of uncertainty when adding features or debugging issues not covered in the curriculum. In the absence of Launch School guidance, I would consult multiple resources looking for the “best” one, wondering if I was doing it the “right way”. However, I gradually learned to rely on my mental models accumulated during Core, to implement features and fixes with more confidence and intention. Having a fundamental understanding of software concepts enabled me to logically develop a deliberate plan for tasks such as processing query results or diagnosing routing issues.

Some Areas of Improvement

  • I’m working on improving my git commit habits and hygiene. Sometimes I would develop for long periods without making commits, and then git add all modified files instead of grouping specific files (or lines) related to a single primary change. This resulted in mixtures of logic, styling, and refactoring changes under one commit.
  • I need to be better about creating branches for new features. I often got excited about some non-essential feature and started coding it without evaluating its impact on the rest of the codebase. In the middle I would end up breaking some core functionality and have to painstakingly get my app back to a working state.
  • I have a general feeling that my app could be more resilient. I do have some tests, but I don’t know enough about QA/QC to properly look for the app’s weak points. There seems to be many ways someone could break it.
  • When trying to translate design ideas from my head to the computer, I felt very limited by my lack of JavaScript knowledge. There are some jarring page refreshes that could be mitigated with AJAX. I also had a tough time trying to configure the autoComplete.js library used for the airport search bar. By the end of Core I expect to be much more comfortable with JavaScript.

Final Thoughts

During the making of this app, I found practical use cases for most of the concepts I’ve learned in Core, and tried to provide specific examples in this article. As a result, I have a better appreciation for why Launch School focuses on the things that don’t change, and why the curriculum is structured as it is. However, I think this perspective is only possible in hindsight, after you’ve passed all the trials on the way here and struggled to build something on your own. So I empathize with those of you who are questioning if it’s worth spending so much time upfront on fundamentals. It’s a difficult question to answer until you’re further along in the curriculum, so I hope this article has given you an idea of what’s ahead and why it matters.

In order to get the most out of Core, you’ll need to trust that the material is high quality, filtered down to the essential topics at an appropriate level of detail, and worth the effort to learn. I personally think so. The curriculum focuses on information with a long half-life: syntax, problem solving, OOP, testing, databases, networking. I think that slowly accumulating these concepts over time will give them a change to mingle in my head, resulting in compounding benefits down the road. I’m aiming to master these fundamentals so can I draw upon them later when learning a new language, troubleshooting a program, or pattern matching in some new domain.

And here’s a trip I’d like to take someday!

Jason’s Dream Trip

(Extra Reading) LS202: HTML and CSS

Confession: I didn’t complete the last few lessons of LS202. This section contains miscellaneous thoughts I had when styling the app, so take them with a grain of salt.

Layout

As penance for not finishing LS202, I did additional practice by trying to give Eight Wonders a mobile-first responsive design that would look decent at 360px and wider. Mobile-first means that we use media queries that take effect when the screen grows wider than a specified min-width property. Most of the changes I made at larger widths involved increasing text size and margin. I spent a lot of time in Chrome Device Mode resizing my window back-and-forth in order to make sure nothing was out of place.

Since the home page header has a photo on the left and a block of text on the right, it was challenging to adapt this layout for both mobile and wider screens. For mobile I used CSS Grid to create one grid-area and assign both the photo and text to the same area, so the text is layered on top. When the screen grows above 600px, I changed the layout to two grid-template-columns so the photo and text could occupy their own grid-area.

I used basic Flexbox and Grid knowledge from LS202 to lay out the app. Here is a diagram of the homepage indicating how they are used.

Home Page Layout

The Chrome DevTools Flex and Grid badges and editors were very useful for debugging and experimenting.

DevTools Utilities for Flex and Grid

User Experience

Since this is a casual app, my goal for user experience was to make it low friction, low effort, and possibly entertaining. That means:

  • After landing on the website, you should be able to quickly and easily create something unique and give it a fun name.
  • There’s no obligation to know any airport codes or do any research. It’s OK to spam the Delete and Randomize buttons, or customize a pre-made itinerary.
  • I don’t have any login pages or ask for personal details. In a future app I will practice implementing authentication functionality.
  • You might discover a city you’ve never heard of before. Maybe looking at the GC Map will spark some desire to see more of the world.

Miscellaneous Notes

  • My first audit with Google Lighthouse scored in the 60s. After resizing images to 2x their maximum expected width (to accommodate Retina displays) and running them through ImageOptim, the score is now 80. There’s probably a lot more I could do to improve performance, such as using WebP and self-hosting fonts.
  • My stylesheets could use refactoring. The selectors are too specific, and there’s probably some repetition in the file.
  • I had to convert my screenshots’ color profiles from my monitor’s profile to sRGB so they displayed properly on the web.
  • Disable Cache in DevTools to immediately see the effect of CSS and JS changes.

--

--