When most people think “ERP backend,” they think Java. Or maybe they think Odoo and Python, but the synchronous, blocking kind. FastAPI barely registers as an option. It’s still seen as a microservices tool. Something you use to ship a quick REST endpoint, not the backbone of an accounting system with a multi-tenant data model and 20 interconnected modules.
We used it anyway. And building Fullfinity on FastAPI taught us things that aren’t in any tutorial. This post is about those things. Not a “why FastAPI is great” pitch, but a realistic look at where it fits, where it bends, and what you need to know if you’re considering it for something more complex than a CRUD API.
Why FastAPI Made Sense for ERP in the First Place
The honest answer is that we wanted async from day one. ERP systems do a lot of waiting. Waiting on database queries. Waiting on external integrations. Waiting on file exports. In a traditional synchronous Python setup, that waiting blocks the process. You need more workers, more memory, more infrastructure to handle the same load.
FastAPI is built on Starlette and runs on ASGI, which means it’s async-native. Not bolted on afterward. The whole request lifecycle supports async/await properly. That matters when your inventory module is hitting the database while your accounting module is processing a webhook from a payment gateway at the same time.
We’ve covered the broader async argument in detail in Async Python in ERP: Why Blocking I/O Is Killing Your Throughput. But the short version is: when you’re building something that needs to scale without rewriting everything, you want async baked in from the start. FastAPI gave us that.
Beyond async, FastAPI’s type-system integration with Pydantic was a genuine win for ERP work. ERP data is complicated. You have invoices with line items, line items with tax codes, tax codes with jurisdiction rules. Pydantic lets you define those shapes explicitly and validate them at the boundary of your API automatically. That catches a category of bugs before they reach your database.
Where FastAPI Gets Complicated Fast
Here’s what the tutorials don’t tell you. FastAPI is opinionated about its entry points and fairly unopinionated about everything else. That’s fine for a small service. For an ERP with 20+ modules, it creates real structural decisions you have to make yourself.
Router organization becomes its own architecture problem.
FastAPI’s APIRouter works well for splitting routes into files. But when you have a CRM module, a Sales module, and an eCommerce module that all expose endpoints and all depend on shared services, you need a strategy for how those routers compose. You need to decide where authentication middleware lives, how dependency injection flows through nested routers, and how module-specific middleware (like per-module permission checks) integrates without duplicating code.
We ended up building a module registry that handles router mounting programmatically. Each module declares its own router, and the registry attaches it during startup. This means you can install only the modules you need, and their routes appear automatically. It’s one of the things that makes the modular architecture actually work in practice.
Dependency injection is powerful but can get messy.
FastAPI’s dependency injection system is one of its best features. You declare dependencies as function arguments, FastAPI resolves them, and you get a clean, testable setup. But in an ERP context, you have dependencies that are deeply nested. A request to create a sales order might depend on the current user, which depends on a session, which depends on a database connection, which depends on a connection pool, which depends on config.
If you’re not deliberate about how you structure those chains, you end up with dependency spaghetti. Every endpoint has a slightly different set of injected dependencies, and debugging why something fails means tracing through five layers of function signatures.
The fix is to be explicit about what lives at what level. Database sessions belong at the request level. Authentication context belongs at the middleware level. Module-specific services belong at the router level. When you enforce that discipline, the dependency graph stays readable.
The Schema Migration Question
Any serious ERP project eventually hits the migration problem. Your data model evolves. Tables get new columns. Relationships change. Foreign keys get added or dropped.
With FastAPI and SQLAlchemy (or most ORMs), the standard answer is Alembic. And Alembic works. But it creates friction at exactly the wrong time. You’re moving fast, iterating on the data model, and you have to stop and generate a migration file, review it, apply it, commit it. For a team building a complex system, that overhead adds up.
This is one of the reasons we built automatic schema management directly into Fullfinity’s ORM layer. You define your models, and the system handles the diff against the live schema and applies changes without you writing migration files. If you’re curious how that works in practice, Zero Migration Files: How Fullfinity Handles Schema Changes Automatically goes into the mechanics.
The point for this post is that FastAPI itself has nothing to say about migrations. It’s not its job. But when you’re building an ERP on top of it, you need a clear answer to this question before you write your first model. Don’t discover your migration strategy halfway through building your accounting module.
Handling Multi-Tenancy at the FastAPI Layer
Multi-tenancy is where most “FastAPI ERP” tutorials completely fall apart. They show you a single-tenant app with a single database. Real ERP deployments are rarely that simple.
There are a few common patterns:
- Separate databases per tenant. Clean isolation, but expensive and hard to maintain at scale. Schema updates need to run across every database.
- Shared database, separate schemas. PostgreSQL handles this well. You switch the search path per request. Still complex to manage.
- Shared database, shared schema with a tenant ID column. Simpler operationally, but you have to be vigilant about filtering everywhere or you leak data between tenants.
FastAPI’s middleware system is actually well-suited for tenant resolution. You can inspect the incoming request (subdomain, header, JWT claim) and set the tenant context before the route handler runs. Then your database layer reads that context and applies the right filters or connection.
The dangerous part is consistency. Every query in every module needs to respect the tenant boundary. This is where an ORM-level solution pays off more than trying to enforce it at the FastAPI layer alone. If tenant filtering lives in your base query class rather than in every individual route handler, you close off the surface area where bugs can appear.
Performance Patterns That Actually Matter
FastAPI is fast by Python standards. But “fast by Python standards” still means Python. When you’re building an ERP that might process thousands of records in a single request (think bulk import, or running payroll for 500 employees), you need to think carefully about where time actually goes.
A few things we learned:
N+1 queries will kill you before CPU will. In an ERP context, you’re almost always fetching related records. An order has line items. Line items have products. Products have inventory records. If your ORM issues a separate query for each of those relationships, a page of 50 orders might generate 500 database round trips. FastAPI’s response speed is irrelevant at that point.
The solution is being deliberate about prefetching and join strategies. Our access-aware ORM handles a lot of this automatically, only loading the fields and relations actually needed for the current request. If you’re working with a different ORM, you need to audit your query patterns regularly. We wrote more about this in Access-Aware ORM: Why Your ERP’s Database Layer Is Probably Doing Too Much Work.
Background tasks for anything that doesn’t need to block the response. FastAPI has a BackgroundTasks system built in. For ERP operations like sending a confirmation email, updating aggregate totals, or triggering a webhook, you don’t need to make the user wait. Queue it as a background task and return immediately. For heavier work, you’ll want a proper task queue like Celery or ARQ, but FastAPI’s native background tasks cover a lot of ground for lighter operations.
Response model discipline. FastAPI lets you define exactly what fields get serialized in a response using response models. Use this. Don’t return your full ORM object and let the serializer figure it out. Define explicit response shapes for each endpoint. This reduces payload size, prevents accidentally leaking internal fields, and gives you a clear contract between your backend and frontend.
Testing FastAPI ERP Endpoints Without Losing Your Mind
Testing in an ERP context is genuinely hard. Your endpoints don’t operate in isolation. Creating a sales order touches inventory, triggers accounting entries, updates the customer record. Integration tests that reflect this reality are slow and complex. Unit tests that mock everything aren’t testing much.
The approach that works best for us is a two-tier strategy.
Unit tests for business logic that can be isolated. Tax calculation, price computation, permission checks. These functions should be pure enough to test without a database. FastAPI’s dependency injection actually helps here because you can override dependencies in tests to inject mocks cleanly.
Integration tests against a real (test) database for endpoint behavior. FastAPI’s TestClient (from Starlette) makes this straightforward. You spin up the app, hit endpoints, and assert on database state and response payloads. The key is test isolation: every test should start with a known database state and clean up after itself. PostgreSQL’s transaction rollback between tests is your friend here.
Don’t try to mock the database in integration tests. You’ll spend more time maintaining mocks than writing actual code. Run PostgreSQL in your CI pipeline. It’s worth the setup.
Module Boundaries and API Design
One pattern that causes problems in ERP systems is treating the entire backend as one flat namespace. Every module exposes endpoints, and those endpoints are all peers. CRM endpoints sit next to Accounting endpoints sit next to Inventory endpoints. It’s fine at first. It gets messy fast.
Better to treat module boundaries as real. Each module owns its namespace. Cross-module operations go through well-defined service interfaces, not by directly calling another module’s internal functions. When your Sales module needs to check inventory availability, it calls an Inventory service method, not an Inventory database query.
This isn’t a FastAPI-specific concern, but FastAPI’s router structure makes it easy to enforce. Each module gets its own router with its own prefix. Internal services are injected via dependencies. The line between “this module’s concern” and “another module’s concern” stays visible in the code structure.
We built this out as part of the modular architecture. The practical outcome is that modules can be installed or removed without breaking each other. If a client doesn’t need the eCommerce module, it’s not loaded, its routes don’t exist, and there’s no dead code sitting in the dependency tree. Building a Multi-Module ERP With Modular Architecture: What Nobody Tells You covers this in more depth if you’re building something similar.
When FastAPI Is the Wrong Tool
Worth saying clearly: FastAPI isn’t always the right choice.
If your team is primarily Django developers and you’re under deadline pressure, switching to FastAPI means learning a new set of patterns under pressure. The ecosystem is smaller. There are fewer drop-in solutions for things like admin panels, auth, and reporting that Django has out of the box.
If you need a quick internal tool and async performance isn’t a concern, the overhead of setting up a proper FastAPI application with connection pooling, async ORM, and dependency injection is more than you need.
FastAPI earns its complexity when you need async from the ground up, when you’re building something that needs to scale, or when you’re building an API that will be consumed by multiple clients (web, mobile, third-party integrations) and you want clean, typed contracts at the boundary. For an ERP platform meant to handle real enterprise workloads, that case is solid.
Conclusion
Building an ERP on FastAPI is doable, and when it’s done well, the result is a backend that’s genuinely performant, type-safe at the boundary, and async all the way through. But it requires making real architectural decisions that the framework doesn’t make for you.
The three things that matter most:
- Decide your module organization strategy early. How routers compose, how services communicate across modules, how middleware applies per-module. These decisions are expensive to undo later.
- Solve the schema migration problem before you have 15 models. Whether you automate it or use Alembic religiously, you need a consistent approach from day one.
- Make N+1 query prevention a first-class concern. FastAPI’s speed doesn’t matter if your database layer is doing ten times the work it should.
Fullfinity is built on exactly this stack. Async Python, FastAPI, PostgreSQL, and an ORM layer designed to make the database-side performance automatic rather than something you have to think about on every endpoint.
If you’re evaluating whether to build on top of it or contribute to it, explore the platform features or browse the blog for more on the architectural decisions behind it.