BLOG: What Actually Happens When You Call a Framework API?
BLOG: What Actually Happens When You Call a Framework API?
There is a point in every developer's career where frameworks stop feeling like magic.
You write:
@Controller('/users')
export class UserController {
@Get('/:id')
async getUser(@Param('id') id: string) {
return this.userService.findById(id);
}
}
And somehow, when a request arrives:
GET /users/42
the framework knows:
- which controller should handle it,
- which method should execute,
- what
idmeans, - which dependencies should be injected,
- how to serialize the return value,
- how to handle errors,
- and how to send the final HTTP response.
The interesting question is:
Where does all of that actually happen?
A framework API often looks deceptively simple.
Behind:
@Get('/:id')
there may be:
Metadata
↓
Registration
↓
Application Bootstrap
↓
Route Resolution
↓
Dependency Resolution
↓
Middleware
↓
Controller Invocation
↓
Serialization
↓
Response
Once you understand this pipeline, frameworks become much less mysterious.
You stop thinking:
"NestJS somehow knows this."
and start thinking:
"There must be a runtime mechanism that registered this metadata and later used it to construct and execute the request pipeline."
That shift in mental model is extremely valuable.
1. A Framework Is Usually a Runtime Built Around Your Code
When people say:
"I'm using NestJS."
or:
"I'm using Spring."
or:
"I'm using ASP.NET Core."
they often imagine the framework as a collection of convenient APIs.
That's only part of the story.
A serious backend framework usually provides a runtime that manages:
Application lifecycle
Dependency graph
Routing
Middleware
Request context
Error handling
Serialization
Configuration
Modules
Plugins
Observability
Your application code becomes a set of components that the runtime knows how to discover and execute.
Conceptually:
Your Code
↓
Framework Metadata
↓
Framework Runtime
↓
Application
This is why frameworks are often described as inversion of control systems.
You don't necessarily call the framework.
The framework calls you.
2. The Difference Between a Library and a Framework
This distinction is worth understanding deeply.
With a library, your code usually controls the flow:
Your Application
↓
Library
↓
Return
↓
Your Application
For example:
const result = bcrypt.hash(password);
Your code decides when the library executes.
With a framework, the direction is often reversed:
Framework Runtime
↓
Your Controller
↓
Your Service
↓
Return
↓
Framework Runtime
You don't normally write:
while (serverIsRunning) {
const request = receiveRequest();
if (request.path === '/users') {
usersController.getUsers();
}
}
The framework owns that lifecycle.
This is the essence of:
Inversion of Control.
3. So What Happens During Application Startup?
Before the first HTTP request arrives, the framework has a lot of work to do.
Imagine:
@Module({
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}
At startup, the framework needs to understand:
Application
↓
Module
├── Controller
└── Provider
It must construct an internal representation of your application.
Conceptually:
ApplicationContainer
RootModule
│
├── UserModule
│ │
│ ├── UserController
│ │
│ └── UserService
│
└── AuthModule
│
└── AuthService
The framework is building a graph.
4. Your Application Is a Dependency Graph
Suppose we have:
class UserController {
constructor(
private readonly userService: UserService,
) {}
}
and:
class UserService {
constructor(
private readonly userRepository: UserRepository,
) {}
}
Then the dependency graph is:
UserController
↓
UserService
↓
UserRepository
If the controller also needs an authentication service:
┌──────────────┐
│ UserService │
└──────┬───────┘
│
┌─────────┴─────────┐
▼ ▼
UserRepository AuthService
The framework needs to construct this graph correctly.
This is the real job of a Dependency Injection container.
5. Dependency Injection Is Not Magic
Consider:
class UserController {
constructor(
private readonly userService: UserService,
) {}
}
You never write:
new UserController(
new UserService(
new UserRepository()
)
);
The framework effectively does something conceptually similar.
The difference is that the framework performs this resolution dynamically based on its application metadata and dependency registrations.
A simplified container might behave conceptually like:
resolve(UserController)
↓
find dependencies
↓
resolve(UserService)
↓
find dependencies
↓
resolve(UserRepository)
↓
construct UserService
↓
construct UserController
The important concept is:
Dependency Injection is dependency graph resolution.
6. Why Does the Framework Know the Constructor Dependencies?
This is where runtime metadata becomes interesting.
Depending on the language and framework, the runtime may obtain dependency information through:
Reflection
Decorators
Generated metadata
Explicit provider definitions
Type information
Code generation
For example, a framework may conceptually transform:
constructor(
userService: UserService
)
into metadata resembling:
UserController
dependencies:
[UserService]
The exact mechanism differs between frameworks.
But the conceptual process remains:
Source Code
↓
Metadata
↓
Container
↓
Dependency Resolution
7. Decorators Are Often Registration Mechanisms
Take:
@Controller('/users')
export class UserController {}
It is tempting to think:
@Controller()
means:
"Create an HTTP controller."
But a decorator itself does not need to create the controller.
It can simply register metadata.
Conceptually:
@Controller('/users')
↓
Metadata:
{
type: "controller",
path: "/users"
}
Then:
@Get('/:id')
getUser() {}
might register:
{
method: "GET",
path: "/:id",
handler: "getUser"
}
The framework later reads that metadata.
This distinction is important:
Declaration and execution are different phases.
8. Startup Phase vs Request Phase
A useful way to understand framework internals is to split the system into two major phases.
Startup
Application starts
↓
Discover modules
↓
Register providers
↓
Read metadata
↓
Build dependency graph
↓
Create instances
↓
Register routes
↓
Start HTTP server
Request
HTTP Request
↓
Router
↓
Middleware
↓
Guards / Interceptors / Filters
↓
Dependency Context
↓
Controller
↓
Service
↓
Response
This distinction explains why some framework operations happen only once while others happen for every request.
9. Route Registration
Consider:
@Controller('/users')
export class UserController {
@Get('/:id')
getUser() {
// ...
}
}
The framework eventually needs something resembling:
GET /users/:id
↓
UserController.getUser
Internally, this could become a route table:
┌────────┬───────────────┬──────────────────────────┐
│ Method │ Path │ Handler │
├────────┼───────────────┼──────────────────────────┤
│ GET │ /users/:id │ UserController.getUser │
└────────┴───────────────┴──────────────────────────┘
The actual implementation can be more sophisticated.
But conceptually, that's what the framework needs to construct.
10. The Request Finally Arrives
Now:
GET /users/42
arrives.
The framework receives something from the underlying HTTP runtime.
Depending on the ecosystem, this could ultimately involve:
Node.js HTTP
Express
Fastify
Netty
Kestrel
Servlet
Tokio
Hyper
The framework doesn't necessarily implement the network stack itself.
It often sits above another runtime.
Conceptually:
Operating System
↓
TCP
↓
HTTP Runtime
↓
Framework
↓
Application
This is another useful architectural boundary.
11. Routing Is a Lookup Problem
The framework now has:
Method = GET
Path = /users/42
and needs to determine:
Which handler?
It might conceptually compare against:
GET /users/:id
and determine:
/users/42
:id = 42
The route matcher therefore produces something like:
Route Match
├── Controller: UserController
├── Method: getUser
└── Params:
id = "42"
Now the framework knows what to execute.
12. But It Still Cannot Simply Call the Method
You might think:
controller.getUser("42");
and we're done.
Not quite.
Real frameworks usually have a request execution pipeline.
For example:
Request
↓
Middleware
↓
Authentication
↓
Authorization
↓
Validation
↓
Interceptors
↓
Controller
↓
Serialization
↓
Response
Each layer has a specific responsibility.
13. Middleware
Middleware usually operates around the incoming request.
Conceptually:
async function middleware(req, res, next) {
// do something
await next();
}
The important abstraction is:
Request
↓
Middleware A
↓
Middleware B
↓
Route Handler
Middleware is essentially a chain.
Conceptually:
M1(
M2(
M3(
Handler
)
)
)
This is sometimes called a middleware pipeline.
14. The Pipeline Is Basically Function Composition
Suppose:
F1 = authentication
F2 = logging
F3 = validation
F4 = controller
The final execution can conceptually resemble:
Or, operationally:
Request
↓
Auth
↓
Logging
↓
Validation
↓
Controller
This is a powerful general programming concept.
Framework pipelines are often compositions of smaller execution units.
15. Dependency Resolution Happens Around Here
Eventually the framework needs the controller instance.
If:
UserController
↓
UserService
the DI container resolves the dependency.
Conceptually:
resolve(UserController)
↓
resolve(UserService)
↓
resolve(UserRepository)
↓
construct dependencies
↓
construct UserController
The resulting instance is then used by the request pipeline.
Depending on framework scope configuration, this instance may be:
Singleton
Request-scoped
Transient
This distinction can have major performance implications.
16. Singleton vs Request Scope
Suppose the framework uses a singleton service:
Application
↓
One UserService instance
↓
Many requests
This is efficient because the object doesn't need to be recreated for every request.
But request-scoped dependencies change the picture:
Request 1
↓
UserService #1
Request 2
↓
UserService #2
Request 3
↓
UserService #3
Now the framework has additional work to perform.
It needs to create a dependency context for each request.
This is why dependency scope isn't merely a design preference.
It can affect runtime performance and memory usage.
17. Parameter Resolution
Now consider:
@Get('/:id')
getUser(@Param('id') id: string) {
return this.userService.findById(id);
}
The method expects:
id
The framework needs to transform:
HTTP Request
into:
"42"
This is another metadata-driven operation.
Conceptually:
Parameter Metadata
Parameter #0
Source = RouteParams
Key = "id"
Then:
Request
↓
params["id"]
↓
"42"
↓
Controller argument #0
The framework is effectively performing argument injection.
18. Validation Is Another Transformation
Suppose:
@Post()
createUser(@Body() dto: CreateUserDto) {}
The incoming request contains:
{
"name": "Quan",
"age": 24
}
The framework may perform:
HTTP Body
↓
JSON Parse
↓
Object
↓
Validation
↓
Transformation
↓
DTO
↓
Controller
The controller therefore doesn't necessarily receive raw HTTP bytes.
It receives an object that has passed through several framework layers.
19. This Is Why Frameworks Feel "Declarative"
You write:
@Post()
createUser(@Body() dto: CreateUserDto) {}
instead of:
const body = JSON.parse(request.body);
if (!body.name) {
throw new Error(...);
}
const dto = new CreateUserDto();
dto.name = body.name;
dto.age = body.age;
await controller.createUser(dto);
The framework moves repetitive infrastructure logic away from application code.
That is the real value of a framework.
It doesn't eliminate complexity.
It centralizes and automates it.
20. The Controller Executes
Eventually:
this.userController.getUser("42");
is invoked.
Inside:
return this.userService.findById("42");
Now the framework temporarily disappears from your mental model.
Your application logic executes.
This is important.
A framework should ideally orchestrate your application without becoming the application itself.
21. Then the Framework Takes Control Again
Suppose the controller returns:
{
id: "42",
name: "Quan"
}
The framework now needs to convert the application result into an HTTP response.
Conceptually:
Controller Result
↓
Interceptors
↓
Serialization
↓
HTTP Response
Potentially:
{
"id": "42",
"name": "Quan"
}
with:
HTTP/1.1 200 OK
Content-Type: application/json
The framework has now completed the cycle.
22. The Entire Request Pipeline
We can now draw the complete conceptual pipeline:
HTTP Request
│
▼
┌───────────────┐
│ HTTP Runtime │
└───────┬───────┘
│
▼
┌───────────────┐
│ Router │
└───────┬───────┘
│
▼
┌───────────────┐
│ Middleware │
└───────┬───────┘
│
▼
┌───────────────┐
│ Guards/Auth │
└───────┬───────┘
│
▼
┌───────────────┐
│ Validation │
└───────┬───────┘
│
▼
┌───────────────┐
│ Controller │
└───────┬───────┘
│
▼
┌───────────────┐
│ Service │
└───────┬───────┘
│
▼
┌───────────────┐
│ Database │
└───────┬───────┘
│
▼
┌───────────────┐
│ Serialization │
└───────┬───────┘
│
▼
HTTP Response
That's the framework's job.
Not business logic.
Orchestration.
23. So What Is the Framework Actually Doing?
At this point we can simplify the entire framework into several major responsibilities.
1. Discover
What components exist?
2. Register
Which component provides what?
3. Resolve
What dependencies does each component need?
4. Route
Which handler owns this request?
5. Execute
How should the request flow through the pipeline?
6. Transform
How do HTTP inputs become application inputs?
7. Serialize
How does application output become HTTP output?
This is a much better mental model than:
"NestJS handles requests."
24. Frameworks Are Metadata Machines
One of the deeper ideas behind decorator-heavy frameworks is:
Your source code contains declarations, and the framework turns those declarations into runtime behavior.
For example:
@Controller('/users')
declares:
This class represents a controller.
Then:
@Get('/:id')
declares:
This method handles GET /:id.
And:
constructor(userService: UserService)
declares:
This component depends on UserService.
The framework combines those declarations.
Conceptually:
Declarations
↓
Metadata
↓
Runtime Graph
↓
Execution
That is a recurring pattern across modern frameworks.
25. This Pattern Exists Outside Backend Frameworks
The same idea appears in many ecosystems.
React
You declare:
<Component />
The runtime determines:
What needs to render?
What changed?
What needs updating?
Spring
You declare:
@Service
and:
@Autowired
The container constructs and wires components.
ASP.NET Core
You register services:
services.AddScoped<IUserService, UserService>();
The runtime builds the dependency graph and request pipeline.
Rust Ecosystem
Even without runtime reflection, frameworks can build similar abstractions through:
Traits
Macros
Code Generation
Compile-time Registration
Generics
This is an important distinction.
The idea is the same.
The implementation mechanism is different.
26. Runtime Reflection vs Compile-Time Generation
There are two broad ways frameworks can obtain this information.
Runtime approach
Source
↓
Metadata
↓
Runtime Reflection
↓
Framework
Advantages:
- flexible
- dynamic
- convenient
Potential disadvantages:
- runtime overhead
- more difficult static reasoning
- reflection limitations depending on language/runtime
Compile-time approach
Source
↓
Compiler / Macro
↓
Generated Code
↓
Runtime
Advantages:
- potentially lower runtime overhead
- stronger compile-time guarantees
- compiler can catch more errors
This is one reason systems programming languages often approach framework design differently from reflection-heavy ecosystems.
27. Framework Magic Is Usually Just Hidden Control Flow
When a framework feels magical, try asking:
Who created this object?
Who registered this route?
Who called this method?
Who converted this parameter?
Who serialized this response?
Who catches this exception?
Who owns this lifecycle?
There is almost always an answer.
The answer might be:
Router
Container
Metadata Registry
Middleware Pipeline
Runtime
Generated Code
But there is an answer.
This is one of the most useful debugging techniques when working with frameworks.
28. Debugging Frameworks From the Inside Out
Suppose:
GET /users/42
returns:
404 Not Found
Instead of randomly changing controller code, reason through the pipeline.
Question 1
Did the HTTP server receive the request?
HTTP Runtime
Question 2
Did the router match the route?
GET /users/:id
Question 3
Was the controller registered?
UserController
Question 4
Was the module loaded?
UserModule
Question 5
Was the dependency resolved?
UserService
Question 6
Did middleware reject the request?
Auth
Validation
Guard
Question 7
Did the controller execute?
This approach transforms framework debugging from guessing into tracing.
29. Framework Performance Is Mostly About the Pipeline
When someone says:
"Framework X is slow."
the question should be:
Which part of the runtime pipeline is expensive?
Possibilities include:
Routing
Dependency resolution
Reflection
Serialization
Validation
Middleware
Request-scoped providers
Database access
Logging
Network I/O
The framework itself may not be the bottleneck.
For example:
HTTP
↓
Framework: 1 ms
↓
Database: 120 ms
Optimizing the framework would accomplish almost nothing.
This is why performance engineering requires measurement rather than assumptions.
30. The Most Important Mental Model
When learning a framework, don't memorize only:
@Controller()
@Get()
@Post()
@Inject()
@Module()
Instead ask:
What does this declaration register?
When is it processed?
Where is the metadata stored?
Who reads it?
When is the object created?
Who invokes the method?
What happens before it?
What happens after it?
Once you can answer those questions, you stop being dependent on framework magic.
You start understanding the runtime.
31. Framework Abstractions Have a Cost
Every abstraction introduces some amount of machinery.
For example:
Simple HTTP Handler
might look like:
Request
↓
Function
↓
Response
while a full framework may provide:
Request
↓
Router
↓
Middleware
↓
Context
↓
DI
↓
Guard
↓
Interceptor
↓
Pipe
↓
Controller
↓
Service
↓
Interceptor
↓
Serializer
↓
Response
The second architecture provides significantly more capabilities.
But it also has more moving parts.
This does not mean:
"Frameworks are bad."
It means:
Abstractions have operational costs.
Good engineering is understanding when those costs are worth paying.
32. When Should You Go Deeper?
If you are building ordinary CRUD APIs, you don't need to understand every internal data structure of your framework.
But deeper knowledge becomes extremely valuable when:
- debugging mysterious behavior,
- optimizing latency,
- designing custom middleware,
- writing framework plugins,
- handling dependency scope,
- diagnosing memory leaks,
- building high-throughput services,
- migrating between frameworks,
- evaluating architecture.
At that point, knowing only the public API is not enough.
33. The Framework Is Not the Application
This is perhaps the most important architectural boundary.
Your application contains:
Business Rules
Domain Logic
Use Cases
Data Rules
The framework provides:
HTTP
Lifecycle
Dependency Management
Routing
Serialization
Infrastructure
Conceptually:
┌────────────────────────────────────┐
│ Application │
│ │
│ Domain │
│ Use Cases │
│ Business Rules │
└──────────────────┬─────────────────┘
│
▼
┌────────────────────────────────────┐
│ Framework │
│ │
│ Routing │
│ DI │
│ Middleware │
│ Serialization │
│ Lifecycle │
└────────────────────────────────────┘
The cleaner this boundary is, the easier the application becomes to evolve.
34. Final Mental Model
The next time you write:
@Get('/users/:id')
don't think:
"The framework handles the request."
Think:
Decorator
↓
Metadata Registration
↓
Application Bootstrap
↓
Route Registration
↓
HTTP Request
↓
Route Matching
↓
Execution Pipeline
↓
Dependency Resolution
↓
Parameter Resolution
↓
Controller Invocation
↓
Application Logic
↓
Serialization
↓
HTTP Response
What looks like one line of code is actually the declaration of a much larger runtime process.
And that is one of the most important things to understand about frameworks:
A framework doesn't remove control flow. It moves control flow from your application code into a runtime that orchestrates your application for you.
Once you understand that, framework internals stop looking like magic.
They become architecture.