Applying clean architecture to my Next.js project

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
so I have been playing around with applying clean architecture to my nextjs side project and I wanted to kind of share with you the approach that I'm taking and also kind of share with you my folder structure where I like to put files where I like to have different um shared libraries Etc so right now the application that I'm going to share with you is super simple it's just a pantry tracking application where you can keep track of things that are in your pantry so if I wanted to for example add some extra cheese that I went to the store and bought I could do that I can also go over here and just go ahead and update that and add too you can also remove things so if I were to click minus here that'll remove it and add it to the out of stock collection that when I go to the store I can see that hey I'm out of eggs um also I was going to add a feature to basically add a running low so I was going to make a button here or something that says like the ability to Market as running low that's honestly all I have right now but I've been focusing more on the structure of the application and where certain modules should live what should know about other modules like what should be dependent on other modules Etc so starting off there's a nextjs application now what I like to do in all my applications is I like to have a Docker compose file where I Define the database right in most of my applications I try to use postgress I plan to use Planet scale I'll just use a MySQL version of this Docker compose what this allows me to do is when anyone wants to work on this project all they need to do is do a Docker compose up and then they get a running database that the nextjs application can connect to now the top level of the project is just a bunch of like configuration files and stuff like that I mean I'm using SST to potentially deploy this application so I have like an SST config where I Define like some cash policies and how to deploy the nextjs site I haven't actually deployed this application anywhere yet but I do plan to I like to use GI up actions to deploy everything so I typically have like a deploy yaml inside my project and this is used for basically setting up some tasks that run automatically when I push to a particular Branch or tag or release in this case when I push the main it's going to go ahead and try to deploy my application so it checks out my code it sets up node 18 installs dependencies and then it tries to run an SST deploy against my AWS environment right so inside get up actions I probably have environment variables set up that give us AWS secret access keys and tokens um so that I can actually do a deployment from GI up actions and that'll basically look at this file uh build my nextjs application and deploy it out which is what this is right here that's more of the infrastructure side of things let's go ahead and look at the structure of my nextjs application so inside my source directory um I'm using App router so the way I like to do things is I've seen people make a components directory and they put like all their components that are related to Pages inside this components directory and then this is typically a slim directory that doesn't have much the way I like to do it is if you have a page then the thing that the page depends on should live right next to it as a file okay so for example if I go to my dashboard everything that's related to the dashboard should be in this dashboard directory so for example I have the page here but then if I were to need a CLI component I would put that client component inside the same directory location like I did here U also this form so I keep it close together as possible so that I don't have to go back and forth between this component directory in my actual dashboard page right I find this much easier to follow and understand because I can come into a new project and I can just open a folder and I know everything that's related to the dashboard will be right here my rule of thumb is that nothing should be in this components directory unless it's used in multiple places if you have a component that's in this component directory and it's only used in one place that is a good signal that you should probably just put it either inside the component itself or in a file that's next to now a second thing I like doing is I like to try to have it be one component per file now if it's a a component that's exported for example we have this items table this item table only has a single component called items table that's exported now if you have multiple subcomponents that this thing depends on it's fine to put them inside of this file like just don't export them I would say if you have a file that has multiple exports in it it's probably also a good signal to have multiple files now that's just the way I like to do it now if you can do that if I can go through here and like common out a component and half of the stuff up here turns red that's a good sign that you could kind of decouple those components right one component has a bunch of dependencies that another one doesn't need and I think that's a good sign of just moving it to a separate file it helps me know when I look at a file that okay these things are all dependencies that are definitely needed by the items table and that's kind of the approach I'm taking with the server actions right so I have aore actions folder so it's not picked up by the URL in the router and I create different files for all my actions I was doing an actions file that had like multiple actions in it but at some point like the files were getting pretty long like you'd have one action that was completely unrelated to another action maybe it was importing some functionality that the other action didn't need so I think just separating them out into separate actions just makes the code a little bit easier to read and understand and I'll walk you through the whole clean architecture part of that in a bit so I have a data access folder and this data access folder is where I'm putting all of my functions that deal with data access such as talking to the database maybe talking to a postgress or or whatever database you're using now as you can tell here I'm actually putting multiple exports in the same directory this is kind of like a repository approach um if you want to kind of follow that pattern but I have basically every function that's dealing with Drizzle drizzle omm is going to live in this items and that's because I only have one items table right so the repository approach that a lot of people take is every table in the database typically has like a file that's associated with it so I got an items table and then that means I have an items repository that I can actually use and inside of this file you have like all your cred methods now alongside that I have a DB Direct which is where I have all my drizzle stuff this is just kind of following the documentation that they provide you with Drizzle and then my schema stuff is here um and then I also have an entities so we'll talk about that in a bit these are like the domain entities or business entities whatever you want to call it but basically this is what I'm using in my application to keep track of whatever logic might be necessary to validate what an item will look like and also provide it some helper methods for kind of um setting things and changing the entity over time and I will dive into this in a little bit um this is more like in the lines of clean architecture for the lib directory I just put any type of third party libraries um for example I have like authentication here I have utilities here basically if you have any third party libraries like let's say you want to talk to AWS you probably want a AWS TS file so you can kind of like wrap those third party libraries so they're not like importing awbs all throughout your code base it just becomes a lot harder to refactor down the road um and then I have a use cases directory so use cases again um I may split this up in the separate files because it's starting to get a little bit unwieldy but for the items whenever someone wants to do something with items such as create an item or remove a item Etc I have these use cases that we call which use the domain entities and then also use persistence methods that are coming from the data access directory to kind of do what needs to be done to increment an item decrement an item Etc now we're going to dive into the whole clean architecture side of things all right so in my items table I have a decrement items action and let's go and see how this is actually Ed so when I click on the minus button this is just going to invoke the server action all right so how this decrement item action is working and a lot of my actions kind of follow the same pattern is if the action requires some type of authentication I'll first get that and then also I get the item ID from the form data and then what I do is I call my use case okay now one thing that you might find really weird is that I'm actually passing the dependencies of that use case inside of the function call so the reason I'm doing this is because if you follow the guidelines of clean architecture your use cases which are sometimes called interactors maybe you might call them controllers they shouldn't know about various persistence level dependencies they shouldn't know how to directly import this data access component so other than this one for some reason I need to refractor this code a little bit I don't have knowledge these things don't have knowledge of the data access layer these things are passed in inside a context using dependency inversion again so this action here this thing is importing the persistence methods the data access methods and then I'm passing them in as needed to these use cases and then the second argument is just the data that the use case needs now typically you should keep this as Primitives now some of the reasons I'm doing this is first of all I want to insulate my server action from my business logic right I don't want any of my business logic or domain logic bleeding in to my front end into my next application I rather have that all separated and I'm doing that via use cases I'm also using dto which I'll talk about um later but again nextjs shouldn't really understand how to talk to a database directly it should just call an abstraction that knows how to talk to a database and knows how to run your your logic if we go back to decrement items use case what this is doing is it's going to get the user from the context again this this use case doesn't know how to check if a user is authenticated doesn't know how to check jts it doesn't know how to check cookies all it knows is that there's a method here that's passed to me that when I call it I may potentially get a user object back and if I don't I can just throw an authentication error so after we check if we're authentic what I do is I do a context. get item and if you look at the return type here it's actually returning a promise of item dto okay so we don't know that we're contacting postgress we don't know that we're contacting a database we don't know we're using drizzle or Prisma all this use case knows is that when I call this method I get some type of data transfer object back from some location this is one of the key parts that will'll keep hitting on with clean architecture is that you're trying to insulate various layers of your application from other things okay it just makes it easier to refactor down the road at least that's what people claim I I kind of agree with it I do think it gives your application some structure but if you notice I kind of have my own custom dto and I use those dto and kind of convert what comes back from Prisma so if you look over here we're calling a g item method which is down here and the way this is work it's using drizzle to get an item from a database and I'm converting that drizzle thing to a dto right so I pass the return object to this 2 dto mapper which basically takes in that drizzle item and returns a custom data transfer object now this can seem a little redundant but what this allows you to do is on the database side of things if you ever decide to refactor or change keys or properties for example let's say quantity was actually going to be called like number of items what this allows you to do is now your business logic doesn't have to change at all right it's just your database layer that is changing how stuff is formatted but your use case is not affected because it just depends on a dto which has a certain format that's kind of the benefit again you can argue this is overkill this is over engineering but this is what I've seen a lot in larger applications dare I say Enterprise applications where they try to follow this pattern so now we have a dto and I've actually seen people return entities from your persistence methods like you could potentially have get item not return a dto you could have it return an item entity and you can do this mapping here like you could have this be like a new item if you wanted to this is just the approach I like to take and I think it works okay so now that we have the dto I have an item entity which takes in some generic Primitives for constructing an item so in our case unpass in in that dto and then we get back an item entity so an item entity is much different from a dto it actually has a bunch of things on it some methods on it that you can invoke as your business entities need to change now this is a super simple example like I mean we're just keeping track of items right but in larger applications your entities might have a ton of different properties a bunch of different validation rules around those properties for example like you might have this need where certain properties depend on other properties to be certain values so you'll have these helper methods that you call which know how to update various things in sync um instead of just like manually changing all them willy-nilly right so if you look up here I'm trying to make all these things private these are all private members so my use case does not have access to actually change any of those underlying properties right or those members of the class I have to use Getters and Setters and I also have to use helper methods if I want to change how this entity works so let's move on from this um this line basically after I create the entity I'm using one of those gter methods to check to make sure that we have enough quantity for us to decrement right so if we have greater than equal to one I'm going to go ahead and just set the quantity to minus one now I probably could have just added a decrement method to this entity which does that logic I mean it's kind of a debate of where to put some of this business logic like does this belong in the use case does it belong in the actual entities I'm not too sure in this case maybe this is an entity thing but regardless at some point when you're changing the entities you want to actually validate them you want to make sure that as these use cases are changing things you're not potentially going to store bad entities in your database so you do want to call like a validate method in my case I have a validate method which basically just uses Zod to verify that the current state of this entity is all valid in our case we make sure that the name you know at least has some length we have a user ID attached to it we have some quantity which should be a minimum of zero and we have a Boolean called is low to verify that hey this is an actual Boolean and defaults def false so after we validate that this item is all good we can just update it using that update method that was passed in using dependency injection and this is a again going to take in a dto if you look at this method this takes in a dto it doesn't take in the entity itself it just takes in a object with some Primitives on it and it knows how to store this in the database if I go back here I have a mapper which takes in an entity and it converts it to the dto that's that's needed okay and it knows how to call these getter methods to kind of take the information from The Entity and transfer it into a dto so that when I pass this the persistence layer or the data access layer the data access doesn't have access to that full entity it just again takes a dto and then finally I return the dto to the front end so that's kind of the approach that I've been taking um again you may look at this and say holy moly this is so much over engineering and sometimes I go back and forth debating with myself of if is this over engineering I have nextjs projects so that was an example of mutating data but also when you're fetching data you should probably also use use cases here I need to come through here and refactor this I'm just um accessing my data access layer directly here technically if you're doing this properly you should be actually invoking a use case here which is going to invoke the data access layer right so you want to have some type of some layered architecture in your backend after explaining this you might think to yourself dude this is over engineering um and if your project is small and your team is maybe one or two people then yeah this might be over engineering what I've seen even on the community code rer project is we had a bunch of people pushing code all the time and it became very very hard to verify a couple of things first of all it came very hard to verify that we're not leaking various things from our data um I think we actually had an instance where we had emails just being leaked out to everyone so everyone who registered into the system their emails were just apparent in the API call so you could load up your Chrome Network tab you can look at the network requests and see everyone's email in the system who registered and the reason that kind of happened is because we didn't have a data access layer we were just kind of fetching items directly from the database using Prisma and since it was a community project and a lot of people weren't really familiar with what they were doing it's very easy for people to just accidentally return back the things that they shouldn't versus this approach first of all we have the data access layer and we can actually use these dto to prevent certain things from even coming over for the wire um like for example let's say this thing had some password or email on it we could just not set it in the mapper we just don't ever return that from the data access layer and for whatever reason we do need it we can make that very explicit in that data layer okay same thing with the use case we have another layer which allows us to basically create a dto to basically prevent what get syn back to the front end right to your next JS application so it's another line of defense and it makes reviewing the code a lot easier right if I see a PR and there's no data access changes I don't have to be on guard of like oh there might be a big security issue with this PR um versus when I do see a PR that's making changes to data access and changing the dtos then I have to be a lot more thorough to make sure that hey maybe double check that nothing is getting leaked okay organizing your code is a good thing um I think people like to just try to code as fast as possible and hack stuff together but I do think there are benefits to following some of these older design patterns that a lot of people use um because I do think they have some good merits to them now another good benefit of this is technically you can use a monor repo and I can pull out some of this stuff and have it live in a completely separate project and kind of test it separately from this nextjs application right so the data access the entities in the use cases technically don't even need to live in this nextjs application but I decided not to good on that approach just yet I don't want to add extra complexity with the monor repo but again the reason for that is if you had multiple JavaScript or typescript things such as like a CLI tool or nextjs application maybe have an Express API those three different things can all import the use cases and persistence methods and invoke them as needed and you kind of get this shared code base of all the business logic that's in its own package anyway I'm curious to hear your thoughts I know this video went kind of long but let me know if you think this is over engineering and also let me know if you think I applied clean architecture improperly to this nextjs code base I'll put the link to this code in the description below so feel free to go through it and critique it how you want other than that that's all I got have a good day and happy coding
Info
Channel: Web Dev Cody
Views: 53,860
Rating: undefined out of 5
Keywords: web development, programming, coding, code, learn to code, tutorial, software engineering
Id: wnxO4AT2N4o
Channel Id: undefined
Length: 20min 15sec (1215 seconds)
Published: Wed Nov 29 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.