Anatomy of a Scalable Python Project (FastAPI)
Key Takeaways
The video demonstrates how to design a scalable Python project using FastAPI, covering topics such as folder structure, configuration management, logging, testing, and dependency injection. It showcases a step-by-step approach to building a scalable Python project with a clean architecture, utilizing tools like FastAPI, Pydantic, SQLAlchemy, and Docker.
Full Transcript
Today I'm going to show you the anatomy of a scalable Python project. Here you see what my project looks like. This is basically the setup that I use for all my production projects. So I'm going to talk about folder structure, config, logging, testing, and tooling that together forms a project that's easy to work with and that can scale to more complexity later on. Now, before we start, if you want to learn how to design software from scratch, grab my free design guide at iron.co/design guide. This contains a seven steps that I use when I design new software. The link is in the video description. Now, when I talk about scalable, what do I actually mean? Well, first, it should scale with size. Basically, your code base is going to grow over time, assuming you're spending some time developing new features. And as your code base grows, you don't want to have to do a ton of refactoring work. It should basically be ready for scale. Another way of think about scalability is think about how it scales with your team. So if you have multiple developers that are involved in a project, you want to make sure that the boundaries are clear, that there are predictable places in the code where you're going to add or work on features. Then there's also scalability with respect to environments. So you want to make sure that things like config is centralized. Uh switching between different environments like development, staging, production should be uh boring in a good way. And finally, there's also scaling with speed. So if you have tests, they should run fast. Uh having a local environment should be really easy to set up. Uh Docker works when you need it. That kind of thing. Over the years, I've worked on many different projects, not just Python, but also uh TypeScript, Java, C++, anything. And of course, depending on the language, things are going to be slightly different. So, what I'll show you today is something that works for me really well in Python. I'd like to start with the folder structure, which should be balanced. You want things to be organized, but you don't want things to be so structured in folders that they're impossible to find. So here's how I have set this up in this particular project. Now there are some folders here at the top uh which is some caching and the virtual environment. Of course these are generated uh but if you scroll down a bit you see that there's an app folder and there's a test folder and these contain well the app basically the source code of your project and then the test of your project. If you look inside the app folder you see that there's subfolders here. Now in this particular example, this is a fast API app and this is what it looks like in the main file. So here there's actually not much going on. Some setup work. We create the app and I register a router. This is an API that handles users. That's all there is to it. And before I dive further into the code, let's actually run this app and see what it does. Now that my app is running, and I'll talk more about how all of this works in a minute. I can actually send requests to this. For example, here I have a call request to create a user. So this created a user or what I can also do is get all the users and that's going to give me that one user that I just created. Now like I said, the way that this is set up and how it's running is done in a specific way that I'll explain in a minute. But first, let me go back to the folder structure. Now this API has a single route which is the users route that you saw just before and that's actually in here. So I have API v1. You may have multiple versions of your API if you're uh developing this incrementally. And here is my user route. So this is basically the HTTP layer. And if you look at the actual code in this route, you see that it's actually pretty basic. Uh there is the uh standard routes that you want. You know, getting the users, creating a user, getting user by ID, uh doing an update, doing a delete, basically the crop operations that you would expect here. And as you can see, the actual code to interact with the database is not even here. That's somewhere else. And I'll talk about that in a minute as well. But the main thing to remember here is that this router is actually pretty empty. There's no business logic here. It's mainly there to specify what the routes are. Then I have a core folder which contains crosscutting concerns, things that are used throughout the application in different ways. One is a config. So for that I'm using pideantic settings which is a really helpful tool for dealing with configs. So here I've created a config class. It has an app name debug mode. There is some database strings users password these kind of things. This can also contain some helpful properties. In this case this constructs the database URL from the database name which is pretty helpful. And then I create an instance of this config that I can then import in other places in my code. Now the nice thing about podantic settings is that it can work naturally with environment variables. So here I'm loading environment variables from a N file and this is automatically used to fill in the values for the user and the password of my database which is nice. Then there's also logging. So this case that's also pretty basic. I just set up a simple logging system here with the time and the logging level and like a formatted message. That's all there is to it. And then I can use this in my code to do logging in a standardized way. So these are the crosscutting concerns core things of my application. Then we have the database and the database will typically have a schema and that's what this looks like. So there is some basic setup work here that I need to do for this particular uh database. As you can see, I use SQL alchemy here and then I have a base class and then I define a user and the user has an ID and a name. So that's the database schema. You could decide to split this up into two files where basically the engine creation and the session creation is actually in a different file than the schema. It's up to you here. I didn't do that because this is a pretty small file but you could do that in a more complex project. Now next to the database we have the models and this again goes back to the API. So here we have pantic schemas for request and response contracts. So this is what you need to pass when you want to create a user which is a name and when you read a user you get back an ID and a name. So these are different things and of course you can add your validation logic here later on if I don't know the user has an email address that needs to be validated or there's other things that you need to check before uh storing the user information and then finally we have the services folder and this contains the actual business logic. So this basically takes the database session and does the actual work. So in this case I have a user service which gets a database session and then it does things with that database session. So it can query it to get all the users. It can get a specific user by ID. It can create a new user update deletes the user. This is the business logic that is actually used by the API. Now you could instead of calling this a surface which is let's say more the architectural name for this you could also call this a repop story if you want to follow more the pattern way of defining things and then finally what we have in the application is the main file which I showed you in the very beginning where we have the basic setup of the API app and we register the route. So that's the application that we have here. Now there's another folder here called test which is also at the top level. So it's not under app. It's a separate folder. And that's actually important that you keep your test separate from your application code. And as you can see in my test folder, I have a test DB which actually sets up a temporary SQLite database which is in memory for testing. And then as you can see I have another folder here API v1 test user. And that actually maps to the same folder structure in my application code. And that's on purpose. I always try to keep my test folder structure structure the same as my application code because that way it's easier to find the test. And inside my test folder, I've created a simple example test. And of course, you can build on this later on. So what this does is that's it sets up a test client. This is a feature from fast API that you can you can use for testing. I override the dependency which I'll talk about more in a minute. And then I test creating and fetching the user. And now if I run piest you see that this test passes because of course I prepared this project so it works. Normally this doesn't work the first time. You have to tweak a bunch of things and make sure it all works. But this time it works. What else is in my project? So as you can see next to these folders we have a bunch of files here. There is uh my project tommo file which contains all the dependencies for this particular project. It contains the required Python version reference to the readme file uh the version the name of the project etc etc. Then I have a m file which contains environment variables for my project. This is normally only used locally. So this can contain things like the database user password, other things like app name, feature flags, stuff like that. And this is what is loaded into the config object by pedantic settings. This is a file that typically you wouldn't commit to your git repository because it contains sensitive information and it's only used locally. When you deploy something like this to the cloud, there's going to be environment variables set in the cloud container instance, which is done differently for each cloud provider. And as you can see in my git ignore file, it's also mentioned here that the m file should not be committed to the repository. Then there is the python version. So this keeps track of uh the python version for this particular project which is helpful for consistent local uh and development environment. Then I have a docker compose which is used for local orchestration. This works with Docker as you can see. It also sets environment variables here. But this actually connects the Docker container of my app. So I can run and use it locally which is what you saw me do in the beginning by calling docker compose up. And then there's the docker file which contains the actual build information for my particular app. So in this case as you can see this works with a UV. So as one of the first steps in my docker file, I actually install UV. I set up the environment path correctly. So UV can be found when you uh want to run it. And then I run UV sync. So that's going to sync the dependencies. Finally, I define the path to the virtual environment. So it knows things like UV corn. I expose the port and then I run the command to start the surface. So that's the docker container. And this directly maps to let's say if you want to deploy this to the cloud, you can simply create a container from this and that's going to start the server in the cloud. And then finally there's a readme file with some information like hey how do you actually start this uh server? How do you uh use it? Some very basic information for your uh developers. There is the uh in this case SQLite database that's been generated by this app. You could also connect this with a database in the cloud later on if you want to. And there's a UV lock file which is generated by UV from the PI project tunnel dependencies. Now why does this structure work so well? Well, uh one thing that's nice is that the API is thin. So it's really only about the routing. The business logic is in a single place, namely the user services. There's uh data concerns that live in my uh database folder and the cross cutting concerns live in a core folder. And then it means you can add features now to this app without having to touch many different files in places you wouldn't expect. Few other things about this structure. So first the config. I mentioned I'm using pideantic settings. Now one thing I did here is that I set a number of values here inside my config. But what this actually also allows me to do is add all sorts of other things here which don't necessarily come from a N file. And that means you can have a bunch of settings here that are maybe less sensitive like the app name for example and you don't have to put all of that in environment variables. You can put them right here in your configuration. The logging part like I said is pretty basic and I simply call this function to set up logging from the main file right here. That's also really simple. But later on maybe you want to move to a structured logging solution where it's actually JSON and we need better observability. Another thing that I want to briefly talk about is something that you see in the router. Uh namely that there is some sort of uh dependency injection here. As you can see the user service is actually created in this function and that gets itself a session from the database. So there's all sorts of dependency injection going on here. The session is injected into the user service and the user service is injected into each route. So we can actually use that surface. So this depends as something that's built into fast API which is why I use this. Alternatively, if you don't want to use this, there are also dependency injection framework like inject for example. These help you connect things in your application and that makes it easier to do the let's say the wiring of your application in a centralized place and you can easily swap out things mocking for test. Uh there is clear uh scopes for lifetime. Dependency injection frameworks can be quite powerful if you have large apps with many crosscutting dependencies. But there are some cons like more boiler plate code. It's an extra mental model for junior developers and can feel quite heavy and uh overengineer for small or medium surfaces. So my rule of thumb typically is to start with the dependency injection that's built into fast API. for this part that works perfectly fine. And then only when it gets really complex and things are connected in many different ways then maybe you can introduce a dependency injection container but only then until that point keep it pretty simple. The surface layer you can basically define these per resource. So in this case there's a user surface and then the routes become very thin translators. they get an HTTP request, they simply call the service and they do an HTTP response. And that means you can also write tests using the user service directly without the actual uh HTTP endpoints. I didn't do that in this particular test. Uh but that is actually something that you could now do. Another thing you can also do pretty easily now since this is a separate service is to swap persistence later. Maybe you want a different database or an external API and then you do the changes here and the uh routes don't have to change at all because it only uses the user service. You can add validation here uh domain rules logging uh basically everything is in the business layer place which is really nice. For the testing bit like I said I'm using an inmemory database and that's all set up in this single file so that these tests are completely isolated. they don't use the production database and then what happens in my actual test is that as you remember I had this dependency injection actually there is also an easy way in fast API to override that dependency for testing so in this case I create a new function called override get user service which creates a testing session but then it can actually use the normal user service because that's injected in there and then I'm overriding the dependency to get the user surface by the override function. And now my test runs independent from the actual production system which is nice. And now of course you can add more test to this setup. So typically what you want to do is first start with a couple of happy path tests like creating and getting a user but then you can also start adding more edge cases like uh validation errors or 404s or things like that. If you write tests like this, avoid sending things to external services in your test themselves, but add integration tests separately. So mock those out basically. In terms of tooling, I'm using UV in this particular project. I'm actually using that on all my projects. Now you can see it being used here in a docker file. If you look at the pi project to file, there is something that is important which is this python path. And this makes sure that Piest knows what the root is of your Python code. So in this case, that's this folder. So that means I can actually import things from the test folder. But also that if I have my user test, I can actually import it like this. And that means that my imports are exactly the same as in my source code where I also import from app models user or whatever. So that way the test import are now exactly the same as the app import which is nice. The docker file sets up the actual surface. So I'm using Python 313 here. I'm installing build essentials. Maybe this line isn't even necessary actually. You could also use a smaller image here like 3.13 slim bookworm for example. And then I'm adding UV setting up the UV environment path. Uh copying over the files syncing dependencies. setting the path for uv exposing the port and then running the uvorn command and the docker compose file actually uses this docker file to run it locally. So the nice thing is that when you call docker compose up like I did in the beginning then actually this creates the exact same environment that you would have in a cloud setup. So your local setup is now really similar to your cloud setup which is pretty nice. So how does this all flow? So let's say a request hits a route. So that happens here, right? My router gets a request. For example, uh listing the number of users. A user service is created with a new session that's injected into the route. And then this calls the list users method of the surface which in turn runs a query on the database session and then returns that as a result which the router then returns as a response to the request. What this structure allows to do is to start really small. You have one service, one router, a simple database setup and then later on you can add more complexity. The boundaries in this setup are really clean. We have the API that does HTTP request services do business logic. Database layer does the persistence and the core layer does cross cutting concerns. Now this is not a let's say fully complete setup. There may be other things that you need. Of course that depends very much on the project that you're running. Right? You may need a deployment script for example to deploy this app to the cloud once it's committed to a specific branch. Uh you may need IDE settings that are specific to this project that you want to share between developers. Maybe you have a scripts folder with custom scripts for data migrations, export, analysis, reporting, those kind of things. But at least this could be a nice starting point for you for your new projects. The example code that I showed you here is in the Git repository. There's a link in the video description. Now, if you enjoyed this video, make sure you give it a like and subscribe to the channel. This is actually a small thing for you, but it really helps me out a lot because that gives me more reach here on YouTube. It means more people can find me and also learn from these videos as as well. But I'd like to hear from you. How do you typically set up your projects? Does it look anything like this or is it completely different? What suggestions do you have for your project setup? Let me know in the comments. Now, I only touched very briefly on software testing here, but there's a lot of things you can do with libraries like Piest. If you want to learn how to set up your tests, this video goes over the basic setup, and it covers a few more advanced things as well, such as parameterized tests and fixtures. Thanks for watching and see you next
Original Description
💡 Learn how to design great software in 7 steps: https://arjan.codes/designguide.
In this video, we break down the anatomy of a scalable Python project using FastAPI and uv. You’ll learn how to set up a clean folder structure, manage configuration with .env, add centralized logging, and write simple but effective tests. We’ll also cover how to use pyproject.toml, .python-version, Docker, and docker-compose for consistent environments and easy onboarding.
🔥 GitHub Repository: https://git.arjan.codes/2025/project.
🎓 ArjanCodes Courses: https://www.arjancodes.com/courses.
💬 Join my Discord server: https://discord.arjan.codes.
⌨️ Keyboard I’m using: https://amzn.to/49YM97v.
🔖 Chapters:
0:00 Intro
0:38 What “Scalable” Means Here
1:40 Folder Structure (Balanced, Not Bloated)
12:33 Config Management (Centralized, Boring, Safe)
13:03 Logging (One Door In)
13:20 Dependency Injection: Frameworks vs FastAPI Built‑Ins
14:58 UserService as the “Business Seam”
15:52 Testing (Fast and Hermetic)
17:08 Python Path and Tooling
20:42 Final Thoughts
#arjancodes #softwaredesign #python
Watch on YouTube ↗
(saves to browser)
Sign in to unlock AI tutor explanation · ⚡30
Playlist
Uploads from ArjanCodes · ArjanCodes · 0 of 60
← Previous
Next →
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
Full stack WEB DEVELOPMENT in 2021 - the ULTIMATE tech stack for FAST web app development
ArjanCodes
FROM PRODUCT IDEA TO SOFTWARE - turn your idea into reality in a few steps
ArjanCodes
Cohesion and Coupling: Write BETTER PYTHON CODE Part 1
ArjanCodes
Build a GLASSMORPHISM React Component - Typescript & Material-UI
ArjanCodes
Observer Pattern Tutorial: I NEVER Knew Events Were THIS Powerful 🚀
ArjanCodes
100% CODE COVERAGE - Think You're Done? Think AGAIN.☝
ArjanCodes
Two UNDERRATED Design Patterns 💡 Write BETTER PYTHON CODE Part 6
ArjanCodes
1000 Subscribers! 🚀 WHY I Started this Channel and WHAT'S NEXT
ArjanCodes
Channel Trailer ArjanCodes - March 2021
ArjanCodes
Exception Handling Tips in Python ⚠ Write Better Python Code Part 7
ArjanCodes
Monadic Error Handling in Python ⚠ Write Better Python Code Part 7B
ArjanCodes
GW BASIC Games I Wrote When I Was a Kid 🎮 Running 30 Year Old Code
ArjanCodes
Why You Should Think About SOFTWARE ARCHITECTURE in Python 💡
ArjanCodes
Uncle Bob’s SOLID Principles Made Easy 🍀 - In Python!
ArjanCodes
QUESTIONABLE Object Creation Patterns in Python 🤔
ArjanCodes
If You’re Not Using Python DATA CLASSES Yet, You Should 🚀
ArjanCodes
CODE ROAST: Yahtzee - New Python Code Refactoring Series!
ArjanCodes
7 UX Design Tips for Developers
ArjanCodes
Going All-in on Software Design in Python + an ANNOUNCEMENT 🎙
ArjanCodes
🎙 Interview with Sybren Stüvel, Developer @ Blender 3D
ArjanCodes
Do We Still Need Dataclasses? // PYDANTIC Tutorial
ArjanCodes
7 Python Mistakes That Instantly Expose Junior Developers
ArjanCodes
Answering Your Most Frequently Asked Python Questions // Q&A 07-2021
ArjanCodes
GitHub Copilot 🤖 The Future of Software Development?
ArjanCodes
More Python Code Smells: Avoid These 7 Smelly Snags
ArjanCodes
Test-Driven Development In Python // The Power of Red-Green-Refactor
ArjanCodes
5 Tips To Keep Technical Debt Under Control
ArjanCodes
Refactoring A Tower Defense Game In Python // CODE ROAST
ArjanCodes
The Factory Design Pattern is Obsolete in Python
ArjanCodes
Why the Plugin Architecture Gives You CRAZY Flexibility
ArjanCodes
Refactoring A Data Science Project Part 1 - Abstraction and Composition
ArjanCodes
Refactoring A Data Science Project Part 2 - The Information Expert
ArjanCodes
Refactoring A Data Science Project Part 3 - Configuration Cleanup
ArjanCodes
Purge These 7 Code Smells From Your Python Code
ArjanCodes
Running A Software Development YouTube Channel
ArjanCodes
Refactoring A PDF And Web Scraper Part 1 // CODE ROAST
ArjanCodes
Refactoring A PDF And Web Scraper Part 2 // CODE ROAST
ArjanCodes
How To Easily Do Asynchronous Programming With Asyncio In Python
ArjanCodes
The Software Designer Mindset
ArjanCodes
NEVER Worry About Data Science Projects Configs Again
ArjanCodes
Powerful VSCode Tips And Tricks For Python Development And Design
ArjanCodes
8 Python Coding Tips - From The Google Python Style Guide
ArjanCodes
What Is Encapsulation And Information Hiding?
ArjanCodes
8 Tips For Becoming A Senior Developer
ArjanCodes
Building A Custom Context Manager In Python: A Closer Look
ArjanCodes
GraphQL vs REST: What's The Difference And When To Use Which?
ArjanCodes
You Can Do Really Cool Things With Functions In Python
ArjanCodes
Announcing The Black VS Code Theme (Launching April 1st)
ArjanCodes
7 DevOps Best Practices For Launching A SaaS Platform
ArjanCodes
Refactoring a Rock Paper Scissors Lizard Spock Game // Code Roast Part 1
ArjanCodes
Refactoring a Rock Paper Scissors Lizard Spock Game // Part 2
ArjanCodes
Things Are Going To Change Around Here
ArjanCodes
Dependency Injection Explained In One Minute // Python Tips
ArjanCodes
How To Setup A MacBook Pro M1 For Software Development
ArjanCodes
A Simple & Effective Way To Improve Python Class Performance
ArjanCodes
How To Write Unit Tests For Existing Python Code // Part 1 of 2
ArjanCodes
How To Write Unit Tests For Existing Python Code // Part 2 of 2
ArjanCodes
Make Sure You Choose The Right Data Structure // Python Tips
ArjanCodes
5 Tips For Object-Oriented Programming Done Well - In Python
ArjanCodes
Next-Level Concurrent Programming In Python With Asyncio
ArjanCodes
More on: API Design
View skill →Related Reads
📰
📰
📰
📰
Stop Using 6 Chrome Tabs for Code Reviews—Do It in Your Terminal
Dev.to · Learn AI Resource
Roadmap for infrastructure/backend development in the .NET ecosystem?
Reddit r/devops
Your NOC Video Wall Is Just a Linux Box Now (Architecture + the 5-Year Math)
Dev.to · Pavel Suchkov
AWS CodePipeline Tutorial: Deploy to EC2 with CodeCommit, CodeBuild & CodeDeploy
Dev.to · Guna SantoshDeep Srivastava
Chapters (10)
Intro
0:38
What “Scalable” Means Here
1:40
Folder Structure (Balanced, Not Bloated)
12:33
Config Management (Centralized, Boring, Safe)
13:03
Logging (One Door In)
13:20
Dependency Injection: Frameworks vs FastAPI Built‑Ins
14:58
UserService as the “Business Seam”
15:52
Testing (Fast and Hermetic)
17:08
Python Path and Tooling
20:42
Final Thoughts
🎓
Tutor Explanation
DeepCamp AI