What I Learned From Three Small Projects
In 2025, I focused on building small, focused projects to sharpen my technical skills and address specific engineering challenges. Large applications often obscure the root causes of issues behind layers of abstraction. By isolating problems in smaller codebases, I was able to confront and resolve foundational misunderstandings. The most effective way to validate technical understanding is to ship isolated, functional units of code. Large-scale applications often obscure foundational misunderstandings behind framework abstractions. By building three distinct "micro-projects" throughout 2025, I isolated and resolved specific engineering deficits regarding static hosting architecture, test environment configuration, and CSS layout algorithms.
This report details the technical implementation, specific failure points, and architectural resolutions for AnimalSounds, CheeseMath, and EthicsFrontEndDemo.
Project 1: AnimalSounds
Repository: AnimalSounds Live Deployment: View Demo
The Engineering Challenge: Browser Autoplay Policies and Static Subpaths
The initial scope was trivial: a DOM manipulation exercise triggering HTML5 <audio> elements via JavaScript event listeners. The implementation utilized a standard Next.js App Router structure.
Failure Point 1: The iOS Audio Context Lock
Upon deployment, the application functioned correctly on desktop Chrome but failed silently on iOS Safari. The application attempted to instantiate the AudioContext and preload assets immediately upon the window.onload event.
Mobile browsers enforce strict autoplay policies. The AudioContext starts in a suspended state by default on WebKit browsers. It cannot transition to running until a distinct user interaction event (specifically touchend or click) occurs. My code was attempting to play audio programmatically before this handshake occurred, causing the browser to reject the request.
Resolution: I refactored the audio engine to implement a "lazy-load" pattern. I removed the automatic initialization and replaced it with a singleton pattern that checks the context state. The first user interaction on the page now triggers a zero-volume buffer playback, which effectively "unlocks" the audio subsystem for the remainder of the session.
Failure Point 2: GitHub Pages Subpath Routing
The second failure involved asset resolution. In a local development environment (localhost:3000), the application resides at the root level. An image reference to /images/cow.png resolves correctly to localhost:3000/images/cow.png.
However, GitHub Pages hosts project repositories on a subpath: username.github.io/repo-name/. When deployed, the application continued to request assets from the domain root (username.github.io/images/cow.png), resulting in 404 errors for all static media.
Resolution:
Hardcoding relative paths (./) proved brittle. The robust solution involved leveraging Next.js environment configuration. I implemented a basePath configuration in next.config.js that conditionally applies the repository name only during production builds.
// next.config.mjsconst isProd = process.env.NODE_ENV === 'production';const repoName = 'AnimalSounds';const nextConfig = {basePath: isProd ? `/${repoName}` : '',assetPrefix: isProd ? `/${repoName}/` : '',output: 'export',images: {unoptimized: true, // Required for static export},};export default nextConfig;
This configuration ensures that asset routing logic remains identical across environments while the build pipeline handles the path rewriting dynamically.
Project 2: CheeseMath (Jest Tests)
Repository: CheeseMath-Jest-Tests Live Deployment: View Demo
The Engineering Challenge: ESM vs. CommonJS in Test Environments
The goal was to implement a robust unit testing suite using Jest. The application logic involved geometric calculations (volume, density, cost) written in modern JavaScript (ES6+).
Failure Point: The "Cannot Use Import Statement" Error
The project utilized ES Modules (import/export) for the source code to maintain compatibility with modern bundlers. However, Jest runs in a Node.js environment, which historically defaults to CommonJS (require).
When running the test suite, the process crashed immediately with SyntaxError: Cannot use import statement outside a module. This occurs because Node.js attempts to execute the source files directly without transpilation, and it does not natively recognize the import keyword in .js files unless the package.json specifies "type": "module" or the file extension is .mjs.
Simply changing the package type broke other tooling. I discovered that the testing environment (Jest) and the production environment (Browser) were effectively two different runtimes requiring distinct build pipelines.
Resolution:
I integrated Babel to act as a bridge between the source code and the test runner. This required installing babel-jest, @babel/core, and @babel/preset-env.
I configured a babel.config.js specifically to target the current Node version used by Jest, rather than the browser targets used for the build.
// babel.config.jsmodule.exports = {presets: [['@babel/preset-env',{targets: {node: 'current', // Transforms ES6 import to CJS require for Jest},},],],};
This pipeline allows the source code to remain clean, modern ES6 while Jest receives the CommonJS code it expects during runtime. It reinforced the concept that "testable code" often requires a dedicated compilation infrastructure.
Project 3: EthicsFrontEndDemo
Repository: EthicsFrontEndDemo Live Deployment: View Demo
The Engineering Challenge: CSS Grid vs. Flexbox for 2-Dimensional Layouts
This project involved building a responsive corporate landing page without the aid of CSS frameworks like Bootstrap or Tailwind. The primary challenge was structuring the main layout grid.
Failure Point: The Flexbox Trap
I initially attempted to layout the entire page using Flexbox. While Flexbox excels at 1-dimensional alignment (distributing items in a single row or a single column), it struggles with 2-dimensional layouts where elements must align on both the X and Y axes simultaneously.
To force a grid structure using Flexbox, I had to use negative margins, complex calc() width percentages (e.g., width: calc(33.33% - 20px)), and container wrappers to handle wrapping behavior. This resulted in "layout thrashing" on mobile devices, where content would overflow the viewport horizontally because the padding calculations did not account for the scrollbar width.
Resolution: Implementing CSS Grid
I scrapped the Flexbox approach for the main container and implemented CSS Grid. The difference in code complexity was substantial.
The Failed Flexbox Approach:
.container {display: flex;flex-wrap: wrap;margin: -10px;}.card {flex: 0 0 calc(33.33% - 20px);margin: 10px;}/* Media queries required to manually resize percentages for tablet/mobile */
The Successful Grid Approach:
.container {display: grid;grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));gap: 20px;}
By using grid-template-columns with auto-fit and minmax, the browser's layout engine handles the mathematical distribution of space. It automatically determines how many columns fit in the viewport and wraps them to the next row when the minimum width (300px) is violated. This eliminated the need for multiple media queries and manual margin calculations, proving that CSS Grid is the superior engine for macro-layout architecture.
Conclusion
These three projects demonstrate that perceived simplicity often masks significant architectural complexity.
- AnimalSounds proved that static hosting environments require strict path configuration strategies (basePath) that differ from local development.
- CheeseMath proved that modern JavaScript requires a transpilation pipeline (Babel) to bridge the gap between ESM source code and CommonJS test runners.
- EthicsFrontEndDemo proved that choosing the correct layout engine (Grid over Flexbox) reduces code volume and technical debt.
Technical growth comes not from reading documentation, but from encountering and resolving these specific environment-level conflicts.