The HNG Internship has been a wild ride. Between the tight deadlines, the late-night debugging sessions, and the sheer volume of new concepts thrown at me, there were moments I questioned my sanity. But looking back, the challenges that nearly broke me are exactly the ones I'm most grateful for. Here are two tasks that stuck with me.
Task 1: The Append-Only Event Store (Individual Stage)
What it was
Build a key-value store from scratch. No PostgreSQL. No MongoDB. Just a file, an index, and the guarantee that data survives a crash.
The spec was simple: accept events via HTTP, store them in an append-only log file, and read them back by ID. Oh, and when the server restarts? Rebuild everything from that file. No data loss.
The problem it was solving
Every database does this. PostgreSQL has its Write-Ahead Log. Redis has AOF. But I never understood why until I had to build it myself.
The problem is crash recovery. If a server dies mid-write, how do you guarantee that old data isn't corrupted? The answer is append-only. You never edit. You only add. If power fails during a write, the worst case is losing that one line. Everything before it stays intact.
How I approached it
I started with the simplest thing that could work. Open a file, write a line, close it. Then I added the index a JavaScript Map that stores where each event lives in the file. Then I added the restart test. Stop the server. Start it again. Make sure the data is still there.
The hardest part was resisting the urge to use SQLite. The spec said no databases. I had to understand file I/O at a level I never had before.
What broke and how I fixed it
Unicode.
I wrote a test with an emoji. Everything looked fine. The event saved. The ID returned. But when I restarted the server, that event was gone. Corrupted.
The problem was string.length. An emoji like "🔥" is one character but four bytes. My offsets were wrong by three bytes. When the server rebuilt the index, it couldn't parse the event.
The fix was Buffer.byteLength(line, 'utf8') instead of .length. That tiny change made everything work. Emojis, Chinese characters, everything.
What I took away
Databases aren't magic. They're files. And indexes. And a lot of careful thinking about bytes.
I also learned that the source of truth is never the index it's the log. The index is just a cache. If it disappears, you rebuild it. That changed how I think about caching in general.
Why I picked it
Because I finally understand what "write-ahead logging" means. I can explain it now, not as a buzzword, but as something I've debugged at 3am on a Tuesday while so many others are in deep sleep.
Task 2: Admin Teams Management API (Team Task)
What It Was
Build a complete admin teams management system from scratch. Create, read, soft-delete teams. Invite members in batches. Generate secure tokens. Send emails. Track pending invitations. Revoke invites. Full CRUD with an invitation system.
Sounds straightforward on paper. It wasn't.
The Problem
The admin portal needed internal teams to control access. Admins needed to create teams, invite members by email, and remove access when necessary. The Figma showed a teams list, a delete confirmation modal, and a full invite flow. Behind the scenes, this meant three new database tables, six API endpoints, email integration, secure token generation, and batch processing.
How I Approached It
I started with the database schema. admin_teams, team_memberships, team_invitations. Foreign keys, status enums, timestamps. That part actually went smoothly.
Then I wrote the service layer. findAll, create, softDelete, inviteMembers, getInvitations, revokeInvitation. Each method had its own quirks, but the real nightmare was inviteMembers.
The requirement: process up to 20 emails per request. Each email independently validated. If two fail, the other eighteen still send. Return a summary. Track failures. Send individual invites via email queue.
What Broke and How I Fixed It
The SHA-256 debacle. The spec demanded cryptographic tokens. Generate a raw token with crypto.randomBytes(32). Hash it with SHA-256. Store the hash. Send the raw token in the email link. When the user clicks, hash the incoming token and compare.
I got this wrong a couple of times. If I remember very well one of the times I think I stored the raw token directly which is a security violation and on top of that pointless. I finally got it right but the URL parsing in the test kept failing because my environment variables weren't set properly during test execution. The test expected https://seil.hng14.com but my .env had https://staging.flowbrand.hng14.com. Took me an embarrassingly long time to realize the test was failing for the wrong reason.
The batch processing race condition. When inviting multiple emails, I needed to check each email against existing pending invites and existing team memberships. But these checks weren't atomic. In theory, two simultaneous requests could race and create duplicate invites. I added proper checks and used database unique constraints as the final backstop.
The APP_URL vs FRONTEND_URL confusion. The spec said APP_URL. The codebase used FRONTEND_URL. My invites were generating links to nowhere until our mentor Jeda helped me traced the environment variable chain and that is when I realized the mismatch.
What I Took Away
Always verify environment variables exist before using them. Fail fast with clear messages.
Tests are not optional. My tests caught the SHA-256 bug, the URL parsing issue, and the batch failure handling. Without them, I would have shipped broken code.
Batch operations need careful error handling. One failure shouldn't sink the whole ship.
Read the existing codebase before writing new code. The FRONTEND_URL vs APP_URL lesson cost me an hour of head-scratching.
Why I Picked This Task
It was my first major backend feature from scratch. No boilerplate, no copy-paste. Just me, the spec, and a deadline. It taught me more about NestJS, TypeORM, and secure token handling than any tutorial ever could.
Final Thoughts
The HNG Internship doesn't hold your hand. It throws you into the deep end and trusts you'll figure out how to swim. Some days I was doggy-paddling. Some days I was drowning. But by the end, I learned how to navigate waters I never thought I'd enter.
Those two tasks both broke me in different ways and I can say both have rebuilt me stronger.
If you're doing this internship and struggling that is good because that's the point. Keep going. The panic you feel at 2 AM debugging a race condition is the price of admission to becoming an engineer who actually knows what they're doing.
Top comments (0)