By Hamza Amir · June 2026
The Problem Nobody Talks About
There is a moment every developer knows — staring at a GitHub repository thinking, "I wish I could contribute to this," then quietly closing the tab. I did that for months. Then I stopped.
This is how I landed 3 merged and active pull requests across two repositories of VoiceyBill — a real-world, open source web app that lets users track income and expenses through voice input, AI receipt scanning, and analytics dashboards. Contributions here are not practice exercises — they ship to actual users on a live production deployment.
That raised the stakes. And honestly, that is what made it worth doing.
Key Terms — Read This First If You Are New
Repository (Repo): A project hosted on GitHub containing all the source code, history, and files.
PR (Pull Request): A proposal to add your code changes to the main project. The primary unit of open source contribution.
Fork: Your personal copy of someone else's repository. You make changes here safely, then open a PR.
Branch: A separate version of the code where you work on a feature without touching the main codebase.
Merge: When the project maintainer accepts your PR and adds your code to the main project.
RTK Query: Redux Toolkit Query — a data-fetching tool used by VoiceyBill's frontend instead of Axios.
ExcelJS: A Node.js library for creating and formatting Excel spreadsheet files programmatically.
JWT (JSON Web Token): A secure token sent with API requests to prove the user is logged in.
CI/CD: Automated checks (lint, build, tests) that run every time you push code. If they fail, your PR cannot merge.
Conventional Commits: A standard for writing commit messages:
feat(),fix(),chore()etc. Keeps history readable.
Part 1: The Roadmap — From Observer to Contributor
This is the exact path I followed. You can follow it too, regardless of your experience level.
Phase 1 — Understand What Open Source Actually Is
Open source means the source code of a project is publicly available for anyone to read, use, and contribute to. When you contribute, you are not just writing code — you are joining a team with real standards, real reviews, and real users depending on your work.
VoiceyBill has three public repositories:
- voiceyBill-web — the frontend (React, TypeScript, Tailwind CSS)
- voiceyBill-server — the backend REST API (Node.js, Express, MongoDB)
- voiceyBill-App
All accept contributions. I worked on couple of them.
Phase 2 — Build Your Basics First
You do not need to be an expert. But you need to be comfortable with these fundamentals before touching a production codebase:
-
Git —
clone,branch,commit,push,pull,merge - GitHub — forking, opening PRs, responding to review comments
- Your stack — at least enough to read and understand existing code
If any of these feel unfamiliar, spend a few days on them first. A rough or broken first contribution is worse than no contribution — it creates work for the maintainers and damages your confidence.
Phase 3 — Find the Right Project
Not every project welcomes beginners. Look for these green flags:
- A
CONTRIBUTING.mdfile that explains how to contribute - Issues labeled
good first issueorhelp wanted - Active maintainers who respond to PRs within a few days
- A project you actually care about or find interesting
VoiceyBill had all of these. The maintainers responded quickly, the issues were clearly described, and the product itself — a voice-powered finance tracker — was genuinely interesting to work on.
Phase 4 — Study the Codebase Before Writing Any Code
This is the most skipped step and the most important one.
Before I opened a single issue, I spent time:
- Reading the existing PRs to understand what "good" looked like
- Studying the folder structure of both repositories
- Observing the team's commit message style (
feat(),fix(), etc.) - Understanding the architecture — how the frontend talks to the backend
When I finally wrote code, I was following patterns I had already internalized — not guessing.
Phase 5 — Claim an Issue and Plan Your Work
Find an issue that matches your skill level. Comment on it to let the maintainers know you are working on it. This prevents two people from solving the same problem simultaneously.
Then plan before you code:
- What files will I need to touch?
- Does this change affect the frontend, backend, or both?
- What is the expected behavior when I am done?
Phase 6 — Build, Test, and Document
Write your code. Test it manually. Run the project's lint and build commands. Then write your PR description as if you are explaining it to a teammate who has no context.
A great PR description includes:
- A summary of what the change does and why
- Step-by-step instructions for how to test it
- Screenshots or recordings if there are visual changes
- The issue number it closes
Phase 7 — Treat Code Review as a Learning Tool
When reviewers leave comments, they are not attacking you. They are doing their job — protecting the quality of a codebase that real users depend on.
Read every comment carefully. Fix what needs fixing. Push the update. Re-request review. Repeat until merged.
The review process is where you grow the fastest.
Part 2: PR #114 — Backend CSV Export (voiceyBill-server)
What Is a Backend? Why Does It Matter?
If you are new to full-stack development, here is a quick mental model:
- The frontend is what users see in their browser — buttons, forms, dashboards
- The backend is the server that runs behind the scenes — it handles data, business logic, authentication, and file generation
When a user clicks "Export CSV," the frontend sends a request to the backend. The backend generates the file and sends it back. The frontend then triggers the download.
This PR built the backend half of that flow.
The Issue
The project needed a way for users to download their full transaction history as a spreadsheet. The issue asked for an API endpoint that:
- Checks that the user is logged in (authentication)
- Fetches only that user's transactions from the database
- Generates a formatted Excel file
- Sends it back as a downloadable response
What I Built
I implemented the complete backend feature following VoiceyBill's existing controller → service → route architecture. Here is what that means in plain terms:
User clicks "Export" in browser
↓
Frontend sends: GET /api/transactions/export
↓
Route → Controller → Service → Database
↓
ExcelJS generates .xlsx file
↓
Backend sends file back to browser
↓
Browser downloads transactions.xlsx
The files I created or modified:
-
Service layer —
exportTransactionsService: queries the database for the authenticated user's transactions, builds an Excel workbook using ExcelJS with bold/centered headers and formatted rows -
Controller —
exportTransactionsController: handles the HTTP request, calls the service, sets the correct response headers -
Route — registered
GET /api/transactions/exportwith authentication middleware so only logged-in users can access it
The spreadsheet columns exported:
| Title | Amount | Type | Category | Date | Payment Method | Description | Recurring | Created At |
|---|
The endpoint details:
GET /api/transactions/export
Authorization: Bearer <your-jwt-token>
Response: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
File: transactions.xlsx
How to Test It (Step by Step)
If you want to test this endpoint yourself after cloning the repo:
# 1. Start the backend server
npm run dev
# 2. Make sure MongoDB is running (local or Atlas)
# 3. Login through the API to get your JWT token
# 4. Call the export endpoint with your token
GET http://localhost:8000/api/transactions/export
Authorization: Bearer <your-token-here>
# 5. Open the downloaded transactions.xlsx in Excel or Google Sheets
The Review Process — CI Failure
The feature worked locally. But after I opened the PR, the project owner voiceyBill flagged that the CI workflow was failing.
CI (Continuous Integration) is an automated system that runs checks on every PR — linting, building, and testing the code. If CI fails, the PR cannot be merged, even if the code itself is correct.
The issue was a missing package-lock.json file. When I added a new dependency (ExcelJS), I did not commit the lock file that records the exact package versions. The CI pipeline could not reproduce the exact environment and failed.
Fix: I added the missing package-lock.json and pushed the update.
** Beginner Tip**
Always commit your
package-lock.json(oryarn.lock) when you add or update dependencies.This file locks down the exact version of every package so that CI servers and other developers get the same environment you tested with. Skipping it is one of the most common first-time mistakes.
The Outcome
✅ Merged
Approved by zainAwan9175 after CI checks passed.
Merged into
mainon June 6, 2026.4 commits. 8 CI checks passed. Backend export live in production.
After the merge, the maintainer commented: "@HamzaAmir1470, now open the PR for the mobile app also." — a signal that the team trusted the work and wanted more.
Part 3: PR #145 — Frontend CSV Export (voiceyBill-web)
Connecting Frontend to Backend
Once the backend endpoint existed (PR #114), the frontend needed to actually call it and trigger the download in the browser. These two PRs are two halves of the same feature — one on each repository.
The Issue
Issue #116 asked for an Export CSV button on the Transactions page that:
- Calls the backend endpoint when clicked
- Shows the user feedback during the process (loading, success, error)
- Triggers a file download in the browser when the backend responds
What I Built
I added an Export CSV button to the Transactions page using RTK Query's lazy query pattern:
User clicks "Export CSV" button
↓
Toast shows: "Preparing export..."
↓
useLazyExportTransactionsQuery() fires the API call
↓
Backend responds with .xlsx blob
↓
downloadCsv() utility triggers browser download
↓
Toast shows: "Export completed" ✅
The Review Process — Three Rounds of Corrections
The feature worked. But the review process taught me far more than the build did.
Round 1: Duplicated Logic
Reviewer Syed-Bilal-Haider-Engineer spotted that my download logic — creating a blob URL, appending a link to the DOM, clicking it, and revoking the URL — was written twice: once for the Transactions page and once for the Reports page.
His suggestion: extract it into a shared reusable utility.
// src/utils/downloadCsv.ts
export function downloadCsv(blob: Blob, filename: string): void {
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
}
** Beginner Tip: The DRY Principle**
DRY stands for "Don't Repeat Yourself." If the same logic exists in two places, it should live in one shared function.
Why? Because if you need to fix a bug in that logic later, you only fix it in one place — not hunt down every copy.
I refactored it. Done. Or so I thought.
Round 2: Missed Caller
A second reviewer — zainAwan9175 — caught that src/pages/transactions/index.tsx still contained the old duplicated implementation. I had created the shared utility but forgotten to update one of the original callers.
Another fix pushed. This kind of mistake is common. It is why code review exists.
Round 3: Wrong Dependency
A third flag: I had accidentally added axios to package.json, even though VoiceyBill deliberately uses RTK Query for all API calls. The project had made a deliberate architectural choice to use one data-fetching tool consistently.
** Beginner Tip: Respect Project Architecture**
Every codebase has intentional technology choices — which HTTP client to use, which state manager, which styling approach.
Before adding a new dependency, check what the project already uses. If there is already a tool for the job, use that one.
Removed the axios import. All three issues addressed.
The Outcome
PR #145 is still under active review with all requested changes addressed, pending final approval from the maintainers. The backend is live. The frontend is ready.
Part 4: PR #171 — Mobile Sidebar Close Button (voiceyBill-web)
The Issue
Issue #132 identified a real usability failure: on mobile, the sidebar opened but had no way to close. Once open, it covered the entire content area with no escape route — affecting every mobile user of the application.
This is a UI/UX bug — the kind of issue that is highly visible, clearly scoped, and very satisfying to fix.
What I Built
I made the sidebar fully responsive without touching any existing desktop behavior.
What "responsive" means for beginners: A responsive UI adapts its layout based on the screen size. The same component behaves differently on a 375px phone screen versus a 1440px desktop monitor.
On screens below 768px (mobile), I added:
- Hamburger button (☰) — floating in the top-left corner, opens the sidebar
- Slide-out sidebar — animates in from the left, same width as desktop
- Close button (✕) — inside the sidebar, explicitly closes it
- Backdrop overlay — a semi-transparent layer behind the sidebar; clicking it closes the sidebar
- Auto-close on navigation — tapping any nav link automatically closes the sidebar
-
Top padding fix —
pt-20on mobile /pt-8on desktop so page content is not hidden behind the floating button
On desktop: nothing changed. Every existing behavior — expanded/collapsed state, navigation links, VoiceyAI card, logout button — remained identical.
The only file changed: src/components/sidebar/index.tsx
Accessibility — Why It Matters
Accessibility means making your UI usable by everyone, including people using screen readers, keyboard navigation, or assistive technology.
For this PR, I added:
-
aria-labelattributes on all new buttons so screen readers can announce their purpose - Proper focus management — when the sidebar opens, focus moves into it; when it closes, focus returns to where it was
- Full keyboard navigation support — users can tab through the sidebar and press Escape to close it
** Beginner Tip: Always Think About Accessibility**
Accessibility is not optional in production software. It is often a legal requirement and always the right thing to do.
The simplest habit to build: every interactive element (button, link, input) should have a clear, descriptive label.
Testing — Why You Must Test Before Submitting
Before submitting, I tested the component across six browsers:
| Browser | Status |
|---|---|
| Chrome (latest) | ✅ |
| Firefox (latest) | ✅ |
| Safari (latest) | ✅ |
| Edge (latest) | ✅ |
| Mobile Safari (iOS) | ✅ |
| Chrome Mobile (Android) | ✅ |
** Beginner Tip: Test on Real Devices**
Chrome DevTools mobile emulation is a good start, but it does not perfectly replicate real device behavior — especially on iOS Safari.
If you are building anything that affects mobile layout, test on a real phone before submitting.
The Outcome
✅ Merged
Reviewed by ahadalireach: "awesome work man."
Approved by project owner voiceyBill.
Merged into
mainon June 5, 2026.One commit. Clean history. No requested changes. First-pass approval.
Part 5: The Full Picture — 3 PRs Across 2 Repositories
Here is how all three contributions connect:
User on Mobile
↓
[PR #171] Mobile sidebar works — user can navigate the app
↓
User goes to Transactions page
↓
[PR #145] Clicks "Export CSV" → Frontend fires API call
↓
[PR #114] Backend generates Excel file → Sends to browser
↓
User downloads transactions.xlsx ✅
Each PR was a standalone contribution. Together, they deliver a complete user-facing feature — end to end, across two repositories, across frontend and backend.
Part 6: Real-World Challenges — What Nobody Warns You About
Challenge 1: The Codebase Feels Overwhelming
Every new codebase does. There are architectural patterns you have not seen, naming conventions you did not expect, and files that seem to do the same thing. This is normal.
The solution is not to understand everything before starting. It is to understand enough to do the specific task you have claimed, then let your understanding grow with each contribution.
Challenge 2: CI Failures Are Not Personal
When CI fails on your PR, it is not a judgment of your ability. It is the automated system doing its job — catching things that humans miss. PR #114's CI failure (the missing package-lock.json) was a one-line fix, but it required understanding why the CI system cares about that file.
Read the CI logs carefully. They tell you exactly what failed and usually point to the file and line number.
Challenge 3: Review Feedback Stings — Then It Helps
Three rounds of corrections on PR #145 felt uncomfortable in the moment. In hindsight, each one made the code meaningfully better:
- Round 1 taught me about the DRY principle in a real codebase
- Round 2 taught me to audit all callers when extracting shared logic
- Round 3 taught me to respect project-level architectural decisions
No tutorial teaches these lessons as well as a real reviewer does.
Challenge 4: Every Project Has Rules You Have to Discover
VoiceyBill uses RTK Query, not Axios. It uses ExcelJS for spreadsheet generation. It has specific commit message formats. These rules are not always in the documentation — sometimes you learn them from a review comment.
When you discover a project rule, write it down. Do not repeat the same mistake.
Challenge 5: Patience Is a Skill
PR #145 is still awaiting final approval. The backend is live. The frontend is ready. The review has been thorough and constructive. Now I wait — and while waiting, I look for the next issue to work on.
Open source moves at its own pace. The maintainers are often doing this work between other responsibilities. Respect their time. Follow up professionally if a PR sits too long without response. Never send repeated messages demanding attention.
Part 7: Strengths and Limitations of Open Source Contribution
What You Gain
Real code review. Engineers you have never met read your code line by line and tell you what is wrong. This feedback is more valuable than any course.
Production exposure. Your code runs for real users on real servers. The accountability is genuine.
Portfolio credibility. A merged PR to an active public project is verifiable proof — anyone can read the code, the comments, and the review history.
Architectural thinking. Working in a real codebase forces you to think about how your change fits into a larger system — not just whether it works in isolation.
Community. You join a network of developers who share standards, norms, and context. After my PRs, the maintainer invited me to open another one. That invitation is worth more than a certificate.
What You Must Manage
Time and patience. Reviews happen on the maintainer's schedule, not yours.
Codebase learning curve. Every new project is a new mental model. Budget reading time before building time.
Rejection. Not every PR gets merged. Sometimes the approach conflicts with the project's direction. This is information, not failure.
Dependency on others. If the maintainer is unavailable, your PR waits. This is part of collaborative software development.
Part 8: Key Takeaways
You do not need to be senior. All three of these PRs were built by someone learning in public. Patience and professionalism matter more than seniority.
Read before you write. The fastest path to a merged PR is understanding what the team already expects — their conventions, their architecture, their tools.
Full-stack contributions are possible. PR #114 was backend. PR #145 was frontend. PR #171 was UI. You do not have to pick a lane on your first day.
CI failures are fixable. When automated checks fail, read the logs, understand why, and fix it. It is rarely as serious as it looks.
Working code is not the finish line. Code also needs to be consistent, non-duplicated, and aligned with the project's architectural choices.
One focused PR beats three scattered ones. PR #171 merged on the first review because it did one thing, documented it completely, and tested it thoroughly.
Every review makes you better. The corrections on PR #145 — three rounds of them — were the most educational part of this entire journey.
The first PR is the hardest. After it, the process is familiar, the codebase is less foreign, and the maintainers are less intimidating.
Part 9: How to Start — Your Action Plan
If you are reading this and have not made your first open source contribution yet, here is a concrete plan:
Week 1: Pick a project you use or find interesting. Read its README, CONTRIBUTING.md, and 5–10 existing PRs. Do not write any code yet.
Week 2: Set up the project locally. Make sure it runs. Read the folder structure until you could explain it to someone else.
Week 3: Find a good first issue. Comment to claim it. Plan your approach. Ask a question in the issue if something is unclear — maintainers appreciate engaged contributors.
Week 4: Build your solution. Test it manually. Run lint and build. Write a detailed PR description. Submit.
After submission: Respond to every review comment within 24 hours. Make changes promptly. Be professional and thankful.
Then look for the next issue and do it again.
Part 10: Further Reading
- Git Branching Strategies — trunk-based development, feature branches, and when to use each
- Writing Good PR Descriptions — how to communicate change intent clearly to reviewers
- Conventional Commits Specification — the standard used by VoiceyBill and most modern projects
- The DRY Principle — why duplicated code creates long-term maintenance problems
- React Accessibility (a11y) — ARIA attributes, keyboard navigation, and focus management
- RTK Query Docs — data fetching and caching in Redux Toolkit applications
- ExcelJS — generating and formatting Excel files in Node.js
- Understanding CI/CD — what automated pipelines check and why they matter
Closing Thought
Open source contribution is not about adding more code. It is about adding structure, care, and precision to a codebase that other people depend on.
VoiceyBill did not make me a better developer by being easy. It made me better by being real — real standards, real reviewers, real users, and real consequences when something breaks.
Three PRs across two repositories. One complete feature delivered end-to-end. Multiple rounds of review that sharpened the code and my thinking.
Two of them are merged. One is pending. All of them taught me something that solo projects never could.
If you have been putting off your first contribution, this is your sign. The codebase is not too advanced. The maintainers are not too intimidating. The issues are not too complex.
Pick one issue. Clone the repo. Build it. Ship it.
That is how you start.
Hamza Amir · github.com/HamzaAmir1470
PR #114 — Backend CSV Export
PR #145 — Frontend CSV Export
PR #171 — Mobile Sidebar
Top comments (0)