‘ll be honest — the first time I set up a Magento 2 project locally and opened it in VS Code, I just stared at the root directory for a solid minute. pub, var, generated, dev, lib, setup — what even is dev? Why is there a setup folder separate from everything else? What does lib have that vendor doesn’t?
I had questions and no good answers. Every tutorial I found either assumed you already knew what these folders were, or buried the explanation under so much background context that I still couldn’t figure out where to actually put my code.
So this is the post I needed back then.
First — Stop Trying to Understand It All at Once
Seriously. When I first googled “Magento 2 folder structure,” every article I found listed every single directory and sub-directory in one giant wall of text. That’s not helpful when you’re new. You end up memorizing names without knowing why they exist.
So instead, I’m going to walk through this the way I’d explain it to a junior dev sitting next to me — by what you’ll actually touch day-to-day, and what you can mostly ignore.
The Root — What’s Actually Here
Open any Magento 2 project and your root looks something like this:
magento2/
├── app/
├── bin/
├── dev/
├── generated/
├── lib/
├── pub/
├── setup/
├── var/
└── vendor/
Nine top-level folders. Let me tell you which ones actually matter for your work.
/app — This Is Where You Live
If you’re building anything custom in Magento 2, you’re spending 80% of your time inside app/. Full stop.
app/
├── code/ → your custom modules go here
├── design/ → your custom themes go here
├── etc/ → global config (env.php, config.php)
└── i18n/ → translation CSV files
app/code/ is where every module you write lives. Magento follows a VendorName/ModuleName convention — so if I’m building something for a client called Nexus and it’s a custom checkout module, it goes in app/code/Nexus/Checkout/. That structure isn’t optional, it’s how Magento finds and loads your code.
Inside that module folder, you’ll have something like:
app/code/Nexus/Checkout/
├── Block/
├── Controller/
├── etc/
│ ├── module.xml
│ ├── di.xml
│ └── frontend/routes.xml
├── Model/
├── Plugin/
├── view/
│ └── frontend/
│ ├── layout/
│ └── templates/
└── registration.php
That registration.php at the bottom? Don’t forget it. I’ve wasted 20 minutes debugging a module that “wasn’t loading” and the file just… wasn’t there. It’s a one-liner that tells Magento this directory is a module. Without it, nothing works.
app/design/ follows a similar pattern for themes:
app/design/
├── frontend/
│ └── YourVendor/your-theme/
└── adminhtml/
└── YourVendor/your-admin-theme/
And app/etc/env.php is where your database credentials, cache backend config, and a few other environment-specific settings live. You don’t edit this by hand very often — Magento’s installer writes it for you — but you’ll read it constantly when setting up staging environments or debugging connection issues.
/vendor — Core Magento and Everything Composer Installed
This is where composer install dumps everything. Magento’s own core modules, Laminas, Symfony components, third-party extensions — all of it ends up here.
vendor/
└── magento/
├── module-catalog/
├── module-checkout/
├── module-customer/
└── ... (there are a lot)
One rule: don’t edit files in vendor/. I know. It’s tempting when you just need to change one line in a core class to fix a bug. I did it. Then ran composer update two weeks later and lost all of it. Everything in vendor gets overwritten by Composer. If you need to change core behavior, use a plugin or preference in your own module under app/code/.
/pub — The Only Folder Your Web Server Should Serve
This trips up a lot of beginners. Your Nginx or Apache config should point to /pub as the document root — not the Magento root. If it’s pointing to the root, your app/etc/env.php (with your DB credentials) is technically reachable from the browser. That’s bad.
pub/
├── index.php → the front controller, entry point for all requests
├── static/ → compiled CSS, JS, fonts (generated, not hand-written)
├── media/ → product images, CMS uploads, etc.
└── errors/ → error page templates
pub/static/ is auto-generated. You run php bin/magento setup:static-content:deploy and Magento pulls your CSS and JS from your module’s view/ folder, processes it, and drops the output here. Don’t put hand-written files directly in pub/static/ — they’ll get wiped on the next deploy.
/bin — The CLI You’ll Use Every Day
bin/
└── magento
That’s it. One file. But it’s the command you run constantly:
bash
php bin/magento cache:flush
php bin/magento setup:upgrade
php bin/magento indexer:reindex
php bin/magento setup:di:compile
If something isn’t working after you add a new module or change config — setup:upgrade and cache:flush are almost always step one.
/var — Logs, Cache, Sessions, Temp Files
var/
├── cache/
├── log/
├── session/
└── tmp/
I live in var/log/ when debugging. Two files you’ll check constantly:
var/log/system.log— general Magento logsvar/log/exception.log— stack traces when something throws an error
If a module isn’t behaving and you can’t figure out why, enable developer mode (php bin/magento deploy:mode:set developer) and watch these logs. The answer is almost always in there.
/generated — Don’t Touch, But Know It Exists
Magento generates PHP code at runtime (or during compile) for things like dependency injection interceptors and factories. It all lands here:
generated/
└── code/
└── Magento/
└── ...
You don’t write anything here. But if you’re getting weird DI errors or a plugin isn’t firing when it should, sometimes deleting the contents of generated/ and running setup:di:compile again fixes it. I’ve done that more times than I’d like to admit.
/lib — Internal Libraries
Magento bundles some of its own libraries here separately from vendor/. This includes internal PHP utilities and web assets (some JS libraries). You’ll rarely if ever need to touch this. Just know it’s not the same as vendor/.
Inside a Module — The Full Picture
Since app/code/ is home, here’s the full layout of a well-structured custom module so you have a reference:
app/code/VendorName/ModuleName/
├── Block/ → PHP classes that pass data to templates
├── Controller/
│ ├── Adminhtml/ → admin-side page controllers
│ └── Index/ → frontend page controllers
├── etc/
│ ├── module.xml → declares the module + version
│ ├── di.xml → dependency injection config
│ ├── frontend/
│ │ └── routes.xml → maps URL paths to controllers
│ └── adminhtml/
│ └── routes.xml
├── Helper/ → utility classes
├── Model/
│ ├── ResourceModel/ → database read/write logic
│ └── YourModel.php → main business logic
├── Observer/ → event listeners
├── Plugin/ → interceptors (before/after/around)
├── Setup/
│ ├── InstallSchema.php → runs once on first install
│ └── UpgradeSchema.php → runs on version upgrades
├── view/
│ ├── frontend/
│ │ ├── layout/ → XML layout instructions
│ │ └── templates/ → .phtml template files
│ └── adminhtml/
│ ├── layout/
│ └── templates/
├── composer.json
└── registration.php → DO NOT forget this
It looks like a lot. But once you’ve built two or three modules, you stop thinking about the structure — you just know where things go.
The Mental Model I Use
When I’m working on something and don’t know where a file should go, I ask myself:
- Is this my custom code? →
app/code/ - Is this a theme file? →
app/design/ - Is this something Composer installed? →
vendor/(don’t touch) - Is this a log or temporary file? →
var/ - Is this a file users or the browser needs to access? →
pub/
That’s honestly it. Once that mental model clicks, Magento stops feeling chaotic and starts feeling structured. Which it is — just in a way that takes a minute to see.
One Last Thing
If you take nothing else from this post: point your web server to /pub, not the root. Write your code in app/code/. Don’t edit vendor/. And when something breaks, check var/log/exception.log first.
You’ll save yourself hours.
Got a specific folder or setup question? Leave a comment below — I check them and actually reply.
