Video guide
20:20 · 26 July 2024
Node.js Refactoring Interview: SOLID Principles on Real Code
Chapters 5 sections
Key concepts in this video:
- 01
1. Reading a Codebase in a Framework You Never Used
The assignment was written in NestJS. Bogdan had never used it. That is normal — companies test you on code you did not write, often in flavors you have not touched.
The way through is architecture, not framework trivia. NestJS implements MVC: controllers, services, modules, DTOs, entities. If you go for a senior backend position, you are expected to know what a controller and a service are and transfer that knowledge to whatever flavor they hand you.
First move: take a top-level view of the whole repo before touching anything. Most of the code was standard. The code smells clustered in one controller — that is where the 45 minutes went.
NestJSMVCcode reading - 02
2. What a Code Smell Actually Is
A code smell is a bad practice, or something that can be improved, measured against a known best practice. It is not an opinion. It is a deviation from a principle you can name.
Example: modifying global state is a code smell in a JavaScript framework built on immutability, because it goes against the framework's own principle. Every smell you call out in an interview should come with the principle it violates.
code smellsbest practices - 03
3. Fix the Endpoint: REST Says POST Returns the Created Object
The controller had a POST method on the transactions path that returned a promise resolving to a string. That is not RESTful. In REST, a POST returns the created entity — here, a transaction object.
First change: fix the return type to the transaction entity. You do not need to know what the framework decorator does under the hood. You need to know what a RESTful endpoint looks like and spot when code is not one.
RESTAPI designHTTP - 04
4. Functions Are Actions — Name Them Like Actions
The method was called something like commission. Functions are actions, and their names should say what they do. Commission is not a good name for something that creates a transaction.
Renamed it to createTransaction. Together with the corrected return type, the endpoint now satisfies the REST contract. Two small changes, both explained out loud, both tied to a principle.
namingclean code - 05
5. Single Responsibility: Kill the Stringify, Extract the Formatting
The handler called JSON.stringify on the response object. Serialization is the framework's job — it happens when the response goes over the network. Doing it in the controller is a stolen responsibility. Delete it, return the object.
Next: a repeated parseFloat-toFixed formatting expression. Repeated formatting logic is the easiest extraction there is — pull it into a small formatAmount arrow function and call it in both places.
That is the single responsibility principle from SOLID applied twice in two minutes. Neither change is clever. Both are visible, explainable, and correct.
SOLIDSRPrefactoring - 06
6. A Get Function That Writes to the Database Is a Side Effect
Two functions, getAmountWithExchange and getAmountWithoutExchange, computed a commission amount — and then inserted a transaction into the database. A get that mutates the database is misleading. That is the side effect to call out.
Apply single responsibility: one function computes the commission amount, a separate function creates the transaction. Use them independently. Modifying state inside something named get will burn whoever reads the code next.
side effectsSRPNode.js - 07
7. DRY: Two Overlapping Functions Become Two Shared Methods
Both functions calculated the commission the same way and both persisted a transaction. The overlap was almost total — the only real difference was one call to an external exchange rate service.
Do the easiest thing first. Extract getCommissionAmount as one async method. Extract persistTransaction as another. Call both from each original function. Repeated code gone, and the actual difference between the two paths is now visible.
That is DRY combined with single responsibility. Also: the nested ternary deciding which path to run got flattened into an if statement. In an interview, the brute force readable version beats the clever one.
DRYSOLIDrefactoring - 08
8. Never Use any Without Saying Why
Under time pressure a parameter got typed as any. That is allowed — if you say it out loud. The line was: we leave any here for now, but I would never use any in a production codebase.
If they ask why, you answer: any breaks type safety, which defeats the point of a static typing system. Offer a generic type as the real fix. In TypeScript interviews they watch for exactly this — silent any is a red flag, explained any is a pragmatic call.
TypeScripttype safetygenerics - 09
9. The Node.js Test: Wrap a Subscription in a Promise
The hardest part. The exchange rate call used a subscribe method with a callback, and the callback's return value vanished into a vacuum — the outer function never returned the transaction. Event-based code does not compose with normal control flow.
The fix is the Promise constructor: return a new Promise, resolve it with the new transaction when the external service finishes, reject it on error. Now the caller can await it like any other async function.
This is where they test real Node.js knowledge. Practice creating promises by hand until it is fast. Do not interview for a senior position in the JavaScript ecosystem without being able to convert callback code to async/await under pressure.
Promiseasync/awaitNode.jscallbacks - 10
10. Leaked Abstractions: Exchange Rate Logic Belongs to the Exchange Service
One more smell: an if statement checking the currency — return an exchange rate of one for Euro, call the service otherwise — sat in the transaction controller. That is exchange rate domain logic scattered outside its domain.
Move the check into the getExchangeRate method, or better, into the exchange service itself: hand it a currency, let it decide. The exchange service had leaked its abstractions into the transaction service. Naming that pattern out loud is exactly the kind of thing that got the strong review here.
abstractiondomain logicservices - 11
11. How to Prepare for a Refactoring Interview
Four things. Know REST in practice — not what a REST API is, but whether a given endpoint is restful and how to fix it. Know SOLID applied to real code, without getting purist. Know JavaScript fundamentals cold: async/await, promises, the Promise constructor. And train speed.
The pressure of an interview makes you nervous and makes you forget. Solidified best practices are what you fall back on. Every decision you make on their code should come from a best practice you can name — do that, and you stand out.
One more thing: the code did not need to compile at every step. Communicating intent matters more than a green build you cannot get anyway. Flag type errors, say you will fix them, keep moving.
interview prepSOLIDRESTfundamentals
Transcript
Hi folks. So today we will go to a real back end live coding JavaScript interview that it's aimed at testing your refactoring skills. For the record, Bogdan got this task in a live coding interview and passed it and today he will share with us the exact steps he followed to pass this interview. As he solves the assignment, Bogdan will be focused on how to apply clean code and solid principles to solve it.
And also review a few core API design, Node.js and JavaScript fundamentals on the way. Bogdan, are you ready? Yes, awesome and uh let's get it going. Perfect. So, yes, this is exactly what I was presented with. Um, it was a senior full stack, but with a very back end heavy kind of position with Node.js. And they sent me this over a couple of days before, and this was basically the instructions, explore the code and the last what is good or bad.
The first thing I did is to take a top-level view of everything in here and I realized it's written with Nest.js. Um, and a lot of the code is pretty standard. Have you used Nest.js in the past? No, not before this interview, no. And what would you tell developers that many times they face the same situation? They they are being uh confronted with a technical assignment like this one that is written in a library they haven't used before, you know, what what was your first uh reaction? How how do you deal with that?
So the first thing is this framework implements MVC. So we see here controllers and modules and uh services, and if we go into these folders, we see like say controllers, this DTO, data transaction objects, entities. And so yes, maybe you never used this before, but ideally, especially when you go for a senior um full stack or back end position, you do know what MVC is, you know what a service is, you know the idea behind it.
So it doesn't matter which kind of flavor they present you with, you should be able to more or less transfer your knowledge there. Mhm. Or at least that's what you expected to do. Now, looking at this code, most of the code smells were in this controller. And I identified that before the interview. in the interview I spent uh on 45 minutes refactoring this. Just for the people watching us that are not familiar with the term of code smell.
How how'd you quickly define it? So basically code smell it's something you could define as a bad practice or something that can be improved, and it's pretty much based on certain best practices that I'll present. So, modifying global state can be considered a code smell in a JavaScript framework that implements immutability, because it goes against the framework principle. So, the first thing you did was to to kind of look at the the the structure of the application, understand this is MVC and you kind of realize that the the transaction controller was the one where most of your activity will be focused on, most of the code smells were. Yes.
Yes, awesome. Awesome. Um, so basically I started here in the interview and we couldn't really execute the code or run it, that was not the purpose. It was just looking at the code, discussing it and applying changes. The first change that you could see here because this is a controller, we see that it's a controller over, there's a decorator here. You don't necessarily need to know exactly what the framework does under the hood, but we see this is a path transaction and it's a post method. So the first code smell we see here is we have a post method on the path transactions.
So applying rest principles, and I added here, what this method should return, it's not a uh promise that resolves to string, it should resolve to an transaction object. Because in rest, the post method always returns the created object. So you need to return an entity, right? And and that's the created object in the case of post, right? Yes. And this is the first thing. You want to know rest principles, you want to know how rest endpoint looks like, what would be non restful, and how to be able to correct this kind of code smells.
The second thing would be naming, functions should have ideally action names because functions are actions, and they should be explicit. which removes the name uh like makes everybody understand what this function does. And commission is definitely not a good name for something that's supposed to create a transaction. So the first thing I did is to rename this to create transaction. And the second thing is I added here transaction object.
And this makes this post method suffice or satisfy our rest implementation. And then I went on to understand what's happening here and try to refactor this applying in a second the dry principle and pretty much the single responsibility from solid. Yes, so if you look at this code, you see we have this JSON stringify method and it stringifies such an object. But stringification it's usually done at framework level when we send over the request.
So, this is a responsibility of the view or the framework normally. We do not need to do this in any in most standard back end frameworks. So, we can even remove this. It's enough for us to return an object, it will get serialized into text when it gets sent over the network by the framework. So, that was that functionality was redundant already. Exactly. The next thing is we see this parse float and to fixed, this is kind of formatting logic and it's repeated.
So, that's the easiest way to extract, right? We could actually extract that really quickly or we could call this format. This is amount. And I'll just extract the logic, this is an arrow function which receives amount and returns parse float of amount to to fix. By the way, they won't allow you to use copilot for any of this. So, better be. You you mentioned you weren't able to run this code on your local machine.
Which means that you basically have to leverage when you're knowledge, you cannot really check if things are actually working out. Except living inside your your linter. You have to be good at actually writing code and making sure it it works without you having to go through the let's say compilation step. Yes, and you need to communicate your intentions because it's not maybe the code doesn't compile or run.
But if you have a solid reason behind it, um then they they get, you know, what's your engineering mind? they said they they do get what your intentions. Okay, so as we see, this is now pretty much straightforward, but um, so this does this doesn't yet create a transaction object yet, and then there's these two functions, get amount with exchange and without exchange that we are running, depending on the currency. So, this kind of logic, it's very hard to read and that's why you always want to move this out into an if statement. Um, of course, there's better ways to do that, but in the interview with the brute first approach is the best. So you just take it out and simplify this and then the next thing is these two functions, what do they really? So that's when I start digging into the rest of the code. Um, yes. And then I went over get amount with exchange and we see right now it's applying some rules to get a commission amount and then it makes this call to this external service and subscribes to it. And whenever that service finishes whatever they need to do, they will basically call our callback which inserts the transaction and then returns the commission amount which was which clearly introduces some side effects that we're going to talk about. Yeah, the first problem is this is a get, it's getting an amount and we are creating a transaction. So that's a side effect that already shouldn't be here. Because if we apply the single responsibility principle, you really want to have one function for creating a transaction and then any other function to get the amount and then use these two separately. The problem with modifying state in the database, modifying the database when you make a get, it's it might be completely misleading. So far so good, and then the next thing it's okay. What about this one? What about the amount without exchange? And we see the same thing, we calculated the commission amount and then we insert the transaction just like before. The only difference is there's no call to that external service. And so we see these two functions are actually very much overlapping. So the easiest thing is to, and I always recommend this, do the easiest thing first, which would be just have a function that computes the commission amount. And then use it in those two methods. Then have another function that creates the transaction and use it in these two methods, and then we actually see what is the difference between two? These two. So basically to not repeat yourself, right?
abstract functionality that's being used in different places into into one function that performs that that action. Exactly. That to be a combination between the eye and the solid principle of single responsibility. So we added this to method, I say it's synchronous and I'm just going to get ahead, take this code out, just like that. Give me a sec. Put it here and see what do you need. I need a transaction input here, copy that.
So far so good and I'm going to just return that, mission amount. And then here we say constant commission amount, let's equal to this, dot, get the commission amount of the transaction. And we do the same in the function above. So we already removed repeated code. That's number one. And then we apply the same thinking to creating this transaction. So, same thinking, we separate repeated logic into a specific parameter, into a specific method.
We call this create persist transactions because we already had a create function at the top, so I call this persist. And this is going to be a bit nasty, I'm just going to take this, put it here and just constant new transaction. Let's see what to this. We're going to return the transaction. So we persist it in into the DB, and then here we're just going to need this apparently transaction input.
That it's of type transaction input. Let's see if that works. You will have to solve type errors very quickly. And we see here, we also need a commission amount on second. I'm make this on a weight. So we have an a sync method and we then need a function because it's a method on a class. That returns again, it returns a promise that resolves to a transaction. That would be the standard. And now, again, you see.
The types are not fully passing, I will, I'll take care of that later. Now, we could actually do this. We could constant new transaction. Let's see what to this. I'm going to have to await this. dot create persist transaction of the transaction. input and the commission amount. And see this commission amount is needed so I'm going to pass it here. And I think commission amount it's of type whatever this is. What is this? Any. Okay, I'll do any, but it never use any without explaining why. You never want to use anything because you're breaking the whole point of a typing system of static type as a whole. Yes, and they wouldn't they won't like that. So I'll just tell them here if I go fast, because it's an interview I need to be fast, I cannot be so explicit as I'm now, I'll just tell them, we're going to leave any here for now, but I'll never use any in a production code base. And then maybe they ask you, why wouldn't you? And then you need to tell them that you're breaking type safety and so on and use a generic type maybe instead of that. And you see, this is a lot easier now. We have get amount with exchange. It's beautiful. And the next thing is, okay, let's do the same to the other function, you remember we had this one. And so here we see, okay, that's done. And now this one creates a transaction, right? So I'm going to go ahead and just to constant new transaction. Let's say we'll to await of this dot. Processed transaction and pretty much the same we had. The transaction input and we had the commission amount. Perfect. So I did that change and now we have this. The problem here is, you see I have this. Retine in this callback, this will actually return in a vacuum because we are passing this as a callback to the subscribe method. They at some point this exchange rate service will finish whatever they need to do. And then call that in the vacuum, there's no return here, so this function doesn't really return a transaction like this one did. And that's a big problem. And so this is one of the most complex part and this is where they will test your no GS knowledge. You need to be able to refactor this into something that works with a sink await and for that we will need to use promises, okay? You want to give it a shot? Okay, so let's give it a shot then. We see we subscribe and what we want, I'm just going to write pseudo code and I did exactly this in the interview, what we want is
is to actually be able me able to return a promise and we want to resolve resolve that promise with a transaction when it is done. And then, you know, it all depends, and of course we want to reject that if there's an error. Reject the promise if there is an error. And it helps if you ever done this exercise before, I highly recommend it to all of you, create be able to create promises, don't even think about interviewing for a senior position in the JavaScript ecosystem if you don't know how to do this, ideally do it fast under the pressure of an interview. We create a new promise, we take all our nasty code, I'll take it here. We won't need this anymore because the promise will reject uh by itself and we're not going to need a commission amount. And basically we put the code we had, and whenever this transaction, it's done, right? That external service finished, they call our code, we resolve our promise with our transaction that new transaction that we created. And if not, we just rejected with the error. So I done this really fast, um, ideally you get to a point where you can do it as fast as I did it. Really important. Know how to use promises to transform subscription-based, event-based things into sync weight that we can use in normal control flow. And by the way, for the people watching us, if you want Paul and I to make a more in-depth video about the promise constructor or any aspects of uh what he mentioned, then uh just let us know in the comment section and we will add it to our list. Awesome, and if you got this far, this is pretty much done, because now, I mean, there's some errors, this exchange rate was actually used to compute a different amount and pass it here, but if they say this is what they actually wanted us to, to do, if they saw this, you probably already passed the interview. Figuring out the details, you can always iron them
amount. And now going back to what we called, right? To our starting point. We we do know these functions, they they get get the commission amount. And then um if this is not two, then we call the external service. And after that, we create a transaction and then we return it. And now we can use the methods we define to actually just do all these steps, so I could do constant, commission amount, go to this dot, get um the commission amount. I think I called it. Get commission amount, give me a sec. Um we're passing the transaction input, so that's number one. And then we have this if statement, so you could create with a let the. I think this was the exchange rate, so put it here to one. And then we call that external service, get the exchange rate and then create the transaction, so this would be constant new transaction equal to await this await this. create persist transaction of I think it was transaction input and commission amount, that's But again, this doesn't have to compile, so if you have some things that don't compile, unless it's something critical they're not signifying and we can find that. Is it commission or commission amount that you want in line 35? Yes, exactly, so I'm going to rename this, commission amount. We passed it in there. And the only thing that we didn't do here is to make this a method to get the actual exchange rate, if the currency, if the transaction currency is not euro, then, you know, then the exchange rate response should be await, call, you know, get the await this dot. And this is why you need to simplify this method even more that we had here. See we had this method that just gets an exchange rate and rather than creating the transaction here to make this even cleaner, cleaner, we just return the exchange rate. So, I'm going to put the exchange rate in. That's the only thing we do, we do not need to um get the commission amount anymore.
Okay, and this is just get exchange rate. Okay, and now going back to our logic, we just call that this.get the exchange rate and we pass in the transaction. And then you can even pass this. I don't like this if statements outside to this method and say, hey, you know what, give me the transaction input, if the currency it's in euro, return one. If it's uh if it's not euro, get exchange rate, if it's euro, just return one.
So to simplify this even harder more and really have that logic of getting the exchange rate in one single place, we could say if this, then just return return one. Because hey, if you have euro, if you are the transaction it's euro to euro, then there's no need to to transfer anything. I go back up here. I did that relatively fast. But because I did that change, I can now do this. constant equals and that's it.
Yeah, and you you also have that that business logic that belongs to the exchange rate domain. It's not scattered under the code base, it's actually within that method. Yes, exactly. And if you if you wanted to be even cleaner, you would move this if, let me scroll back to the actual um service. Because they should handle it. We just give them our currency and we shouldn't have this if statement here because it's really uh logic that was um um yeah, that it's it's it's abstractions that belongs to the exchange rate service or domain or whatever not.
Yeah, yeah. So we could say that the exchange service leaked some of its abstractions into this service. That's it. If you were getting this far and this is where I got with them, they were uh more than happy to make a very generous offer. I think they offered around 10 to 20 on top of what their budget was. Uh because the review from the interview was very good. I was interviewed by an engineer that had around 12 years of experience in in the backend.
It was a financial institution, pretty well known startup in Europe. So one thing for the people watching us, uh, you you definitely have a lot of experience, not only being a software engineer yourself, but training software engineers, Bogdan and you are quite fast and you do have a very solid understanding of the backend, right? But what would you tell people when, uh, you know, because there's a few things here, right? You number one, you are in the red zone. The red zone being you are interviewed and you are interviewed on code that you haven't written.
So, forget everything you know, right? Just number one, go back to the fundamentals, right? That's what I've said. You talk about the promise constructor, you talk about the solid principles applied to the code. So what would you, you know, what are the things that they the developers, software engineers watching us should do for them to increase their chances of passing or for to prepare for such an interview.
So, to review, you want to know where you are, you have the principles, applied the principles, not only like what's rest API, but actually like is this restful? Is this not? You want to know uh solid in practice. Um there's not so many examples of this in real uh code bases and especially on the JavaScript ecosystem, but ideally you know how to mole us look at some code and apply some of the solid without getting too purist in it.
And you definitely want to know like JS fundamentals like sync, right? sync/ wait and promises. You want to know these things because with the pressure of the interview, you'll most likely get nervous, forget things and if you have this best practices very solidified in your brain, you can always fall back to those. No matter how nervous you are, you can always fall back to those, yeah? And then train a bit of speed, you want to be fast in the interview, but if you mention all those, if you really touch on all those and show them how you can apply them, very important, then you will definitely stand out. But every decision you make on this code base should come from you reaching out to a
best practice that you know. Thank you, Bogdan. One last thing I'll say for the people watching us, if you are interviewing right now and you're looking for a reliable way to find out what are your technical gaps, things you could do to improve your interview performance, then check out the free tenkle assessment, which will help you find out exactly where you're at and what are the steps you should take for you to perform at a higher level.
Not only in tenkle interviews, but overall as a software engineer. Once again, thank you so much, Bogdan, and we will see you folks in the next one.