DEV Community

Cover image for Two Backend Tasks From My HNG Internship That Stuck With Me
ibraheembello
ibraheembello

Posted on

Two Backend Tasks From My HNG Internship That Stuck With Me

I spent the last few months in the HNG internship writing backend code, breaking things, and figuring out how to put them back together. By the end you collect a lot of tickets, a lot of pull requests, and a few late nights you would rather forget. But when I sat down to write this, two tasks came back to me before any of the others.

One I did on my own. One I did as part of a team. Both took longer than they should have, and both taught me something I did not expect going in. This is the honest version of how they went.

Task one: securing a single backend for both a CLI and a web app

What it was

This was my Stage 3 task. I was asked to build a backend for a small analytics product I will call Insighta. The twist that made it interesting was that the same backend had to serve two completely different clients at once: a command line tool that an analyst would install and run from their terminal, and a web portal they would log into from a browser. Same data, same permissions, two front doors.

So I ended up building three things that had to agree with each other: the API itself, a global npm CLI you could install with one command, and a Next.js web portal.

The problem it was solving

The real problem was not "build an API". It was "build one security model that two very different clients can both trust". A browser and a terminal authenticate in almost opposite ways. A browser is happy with cookies, redirects, and sessions. A command line tool has none of that. There is no browser tab, no cookie jar, no obvious place to safely store a login.

If I got this wrong, I would either have to build two separate auth systems and keep them in sync forever, or I would end up with a CLI that asked people to paste long lived secrets into a config file, which is exactly the kind of thing that leaks.

How I approached it

I picked GitHub OAuth as the single source of identity so I was never storing raw passwords for this product. On top of that I added role based access control with two roles, admin and analyst, so the same token could be checked the same way no matter which client sent it. I wrapped the API in the usual production hygiene: helmet for headers, structured logging with pino, CSRF protection, rate limiting, and an explicit API version header so clients could not be surprised by changes.

For the web portal I used HttpOnly cookies and a double submit CSRF token, kept everything on a single origin using rewrites so the cookie rules stayed simple, and made the UI refresh its token automatically when it hit a 401 instead of dumping the user back on the login screen.

The CLI was the part I had to really think about.

Suggested caption: One backend, two front doors. The CLI and the web portal share a single identity and one security model.

What broke and how I fixed it

OAuth assumes a browser. The whole dance is built around redirecting a user to a login page and then redirecting them back to a URL your server controls. A terminal has no URL to redirect back to. My first few attempts tried to bend the normal web flow onto the CLI and it just did not fit. Either the login could not complete, or I was tempted to store a long lived token in plain text, which I did not want to do.

The fix was to use the authorization code flow with PKCE, which is the variant designed exactly for clients that cannot keep a secret. When you run the login command, the CLI spins up a tiny local web server on a loopback address, opens your browser to GitHub, and uses that local server as the redirect target. GitHub sends the code back to your own machine, the CLI exchanges it using a one time code verifier that never leaves your computer, and only then do you get tokens. Those tokens get written to a credentials file in your home directory with locked down file permissions so other users on the machine cannot read them.

Suggested caption: The CLI login flow. A tiny local server on your own machine becomes the redirect target, and the secret verifier never leaves your computer.

Once that clicked, both clients were finally speaking the same language. The CLI and the browser were getting identity from the same place, the API checked every request the same way, and I was not storing a single raw password or long lived secret in plain text anywhere.

I deployed the API on AWS App Runner so it had a real home and was not just running on my laptop.

What I took away

Authentication is not one problem, it is a different problem for every kind of client, and the trick is finding the one model underneath that all of them can share. Before this task PKCE was just an acronym I had seen in docs. After it, I understood why it exists and what it actually protects you from. I also learned to be suspicious of any design that asks a user to paste a secret into a file. There is almost always a better flow.

Why I picked it

I picked this one because it is the task where I stopped copying patterns and started understanding them. I could not google my way to a CLI OAuth flow that matched my exact setup. I had to read the spec, understand the reasoning, and build it. That is the moment the internship started feeling like engineering instead of homework.

Task two: fixing a login flow that was broken between everyone's code

What it was

The team task was a product I will call SEIL, a NestJS backend that several of us built together in a shared repository. We did not fork and send pull requests from the outside. Everyone had write access and pushed feature branches straight to the same repo, which means your code lived right next to everyone else's and had to get along with it.

My ticket was BE-005: login, logout, and session management. On paper it sounds small. In practice it was the ticket where all the separate pieces other people had built were supposed to finally connect, and that is exactly where the trouble was hiding.

The problem it was solving

Registration already existed. Login already existed. Logout already existed. On their own, each endpoint looked finished and each had been merged. My job was to add account lockout after repeated failed logins, tidy up session handling, and make the whole login lifecycle actually usable from end to end.

The catch is that "each endpoint works on its own" and "the flow works end to end" are two very different statements, and the gap between them was my entire ticket.

How I approached it

I started by reading other people's code instead of writing my own. I traced a single user from registration, through login, to calling a protected route, to refreshing their token, to logging out. I wanted to walk the full path before touching anything.

For the actual lockout feature, the database already had unused columns sitting there waiting: a failed attempts counter and a locked until timestamp. I wired up the policy of five failed attempts locking the account for one hour, made a locked account respond with a clear 423 status instead of a generic error, and made a successful login reset the counter and stamp the last login time. I also wrote unit tests for every branch of that logic, because lockout is the kind of thing you only find out is wrong when a real user gets locked out by accident.

What broke and how I fixed it

Two things were quietly broken, and both of them lived in the seams between different people's work, which is why nobody had caught them.

The first: protected routes required a session record in Redis, but only the registration endpoint actually wrote that record. So if you logged in normally, your token was valid, but the moment you tried to log out or load your profile you got a 401. The login path and the guard had been built by different hands and nobody had walked the full path between them. I fixed it by making token issuance always write the Redis session, no matter whether you arrived through register, login, or refresh, so the guard always had something to find.

The second was sneakier. Login set the refresh token as an HttpOnly cookie, which is the secure thing to do, but it means JavaScript cannot read it. Meanwhile the refresh endpoint only knew how to read the token from the request body, and the login response did not echo the token anywhere a client could grab it. So the secure design and the refresh endpoint were each correct on their own and completely incompatible together. There was no path a real browser client could actually take to refresh a token. I added cookie parsing on the server, made the refresh endpoint accept the token from either the body or the cookie, and made logout also delete the Redis key so an access token died the instant you logged out instead of lingering for fifteen minutes.

Suggested caption: The same flow before and after. A valid token used to get rejected because no session was ever written. The fix made token issuance always create the session.

None of these were dramatic bugs. They were the kind that only show up when you stop testing one endpoint at a time and start testing the way an actual user moves through the system.

What I took away

In a team, the bugs are not usually inside any one person's code. They are in the spaces between, where your assumptions and a teammate's assumptions quietly disagree. Everyone can be individually right and the product can still be broken. The most useful thing I did on this ticket was not writing code, it was reading everyone else's carefully and walking the full path before I trusted it. I also got a lot more comfortable working in a repo with strict standards, a pre-commit hook, automated scanners, and reviewers who would send a pull request back if it grew too large. Discipline that felt slow at first is the only reason a shared codebase with many hands in it stayed sane.

Why I picked it

I picked this one because it changed how I think about teamwork. I came in assuming a team task means splitting the work into pieces and gluing them together at the end. This taught me the gluing is the work. The interesting, hard, and genuinely valuable part was making independent pieces trust each other, and you only get good at that by reading more than you write.

Closing

If I tie these two together, they are really the same lesson seen from two sides. The solo task was about finding one model that two different clients could share. The team task was about finding the seams where separate pieces failed to agree. Both came down to the same instinct: do not trust that things work together just because they work apart. Walk the whole path yourself.

That instinct is the most useful thing I am taking out of HNG, more than any single framework or tool. Thanks for reading.

Top comments (0)