Skip to content

feat: allow to access decorators#5768

Merged
Eomm merged 15 commits intofastify:mainfrom
jean-michelet:allow-access-decorators-before-boot
Mar 29, 2025
Merged

feat: allow to access decorators#5768
Eomm merged 15 commits intofastify:mainfrom
jean-michelet:allow-access-decorators-before-boot

Conversation

@jean-michelet
Copy link
Copy Markdown
Member

@jean-michelet jean-michelet commented Oct 24, 2024

New proposal

Allow to access decorators on fastify, request and reply thanks to accessor getDecorator<T>(decoratorName):

fastify.getDecorator<SomeFeature>('someFeature') // great for dependency check at registration
request.getDecorator<SomeFeature>('someFeature')
reply.getDecorator<SomeFeature>('someFeature')

The goal is to avoid module augmentation for TypeScript users, because augmentation type the decorators globally on instances. It causes issues if developers want to instantiate several Fastify servers or to leverage encapsulation for their business logic.

Some users might also use this for Early Plugin Dependency Validation and better error handling for missing decorators. Refer to the doc update.

This proposal is mostly obsolete

I added tests for typing, encapsulation, and different kind of decorator values (function, getter/setter, primitive) for instance, request and reply.

We currently leverage module augmentation for decorators, that doesn't give any information about the level of encapsulation, worse, it does pollute the whole module. I think this is bad, doesn't bring a lot of type safety and is inconvenient.

Maybe we can think about decorators as dependencies in some way, nice features added by plugins. We should be sure that these decorators are available before the application even start. This is possible to do this check with fastifyPlugin:

module.exports = fp(plugin, {
  decorators: {
    fastify: ['plugin1', 'plugin2'],
    reply: ['compress']
  }
})

But currently does not give any typing information, and I get headaches just imagining trying to educate TypeScript about it...
There is a simple solution to that imo, and I would like your feedback.

We could export the decorators from core plugins instead of just augmenting the global instance and request/reply:

export interface StaticReplyDecorators {
  sendFile(filename: string, rootPath?: string): FastifyReply;
  download(filepath: string, filename?: string): FastifyReply;
}

// Also usefull for Vanilla projects with fastifyPlugin
export const STATIC_REPLY_DECORATOR_NAMES = ['sendFile', 'download']

We can keep the actual module augmentation for people who like working this way.

We could use this feature this way at plugin registration:

// throw an error if one of the decorator doesn't exist
const { sendFile, download } = fastify.getDecorators<StaticReplyDecorators>(STATIC_REPLY_DECORATOR_NAMES)

More realist use case:

const { sendFile } = fastify.getDecorators<StaticReplyDecorators>('sendFile')
fastify.get('/', (request, reply) => {
  return sendFile.call(reply, 'hello.html')
})

Yes, we have to leverage call, but it does work, doesn't break anything and causes much less issues than module augmentation imho. There is just exports to create if we want to provide appropriate typing, I can work on it.

EDIT:
Maybe leveraging .call is slightly less performant? There are probably solutions to this. We can also return a slightly different decorator that take request or reply as first argument: sendFile(reply)

Do you see any value in this feature?

Sorry, I had closed the original PR and removed the branch, so could not reopen after changing my mind.

@github-actions github-actions bot added documentation Improvements or additions to documentation typescript TypeScript related labels Oct 24, 2024
@jean-michelet
Copy link
Copy Markdown
Member Author

jean-michelet commented Oct 24, 2024

I know it's strange to work like this, because the purpose of decorators is precisely to... decorate...
But, for me, it's important not to force Fastify users to use decorators to get features from core plugins.

@jean-michelet jean-michelet changed the title Allow to access decorators before start. feat: allow to access decorators before start. Oct 24, 2024
Copy link
Copy Markdown
Member

@metcoder95 metcoder95 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I shared same thought on a similar PR, but I'm having a little bit of troubles to see the real value of the feature.

As per this example:

const { sendFile } = fastify.getDecorators<StaticReplyDecorators>('sendFile')
fastify.get('/', (request, reply) => {
  return sendFile.call(reply, 'hello.html')
})

It really seems a decorator would just fit better right there.

Is there any particular use-case you've in mind to support with the feature?

@jean-michelet
Copy link
Copy Markdown
Member Author

It does fit better on a Vanilla JS project, but not with typescript imo. The goal is not to create a new design, but to tell the users that we are not forcing them to use decorators with module augmentation and there is an alternative if they prefer.

@jean-michelet
Copy link
Copy Markdown
Member Author

getDecorators, in my mind is getUtils or getConfig. The reference to decorators is accidental, it's just an adaptation around the existing, wich can be stated clearly in the doc.

Copy link
Copy Markdown
Member

@Eomm Eomm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could export the decorators from core plugins instead of just augmenting the global instance and request/reply:

What about decorator with a dynamic name?

The title says allow to access decorators before start, but this is already possibile, so: are we solving a TS issue?

const { sendFile } = fastify.getDecorators<StaticReplyDecorators>('sendFile')
fastify.get('/', (request, reply) => {
  return sendFile.call(reply, 'hello.html')
})

isn't this example already doable?

const sendFile = fastify.sendFile as StaticReplyDecorators
fastify.get('/', (request, reply) => {
  return sendFile.call(reply, 'hello.html')
})

I think we need more use cases that this feat is solving to get it merged.

@jean-michelet
Copy link
Copy Markdown
Member Author

jean-michelet commented Oct 25, 2024

The title says allow to access decorators before start, but this is already possibile

Not request and reply decorators, at least not in a formal way, we could use key symbols kReply and kRequest but I wouldn't recommend to depend on internal components.

isn't this example already doable?

const sendFile = fastify.sendFile as StaticReplyDecorators
fastify.get('/', (request, reply) => {
  return sendFile.call(reply, 'hello.html')
})

Unless the plugin has decorated the fastify instance with a function that depends on Reply as this context, your example will return undefined. You don't have a check to ensure the decorators exists at registration, although achievable with fp, isn't it better to resolve runtime/compilation check in one place?

But yes, we could allow via an option to set decorators to the current instance instead of Request/Reply prototypes/props.

I also realize thanks to your comment that my implementation is wrong, because two decorators a can exist on instance, request and reply:

fastify.decorate('a', 'from_instance')
fastify.decorateRequest('a', 'from_request')

fastify.getDecorators('a') // from_request is never found

So, we should have getRequestDecorators and getReplyDecorators.

Copy link
Copy Markdown
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should just expose:

  • getRequestDecorators
  • getReplyDecorators

as I don't think we should mingle them together in a single API. There is no need to have this API for the fastify instance because one could always do instance[name].

@jean-michelet
Copy link
Copy Markdown
Member Author

Yes, I agree.

@jean-michelet
Copy link
Copy Markdown
Member Author

There is no need to have this API for the fastify instance because one could always do instance[name].

There's an advantage to access it via getFastifyDecorators as there's a check to verify that the decorator is accessible. But unlike Request/Reply, there is no need to expose it. So I can understand you would prefer to leave the responsibility to the end-user, let me know and I remove it.

@jean-michelet
Copy link
Copy Markdown
Member Author

jean-michelet commented Oct 26, 2024

I understand the skepticism about giving access to Request/Reply decorators, and there's no hurry, I can work on convincing you more.

This might not be used directly by users, but by a plugin offering a slightly different architecture to work with Fastify (I need to think/read about it). But for this to happen, it would be great to be able to offer adapters capable of wrapping the many features of Core plugins on Request/Reply as changing/duplicate an entire ecosystem is not an option. This my main concern and the reason for this PR.

In the meantime, here are a few examples.

A logger plugin that decorate instance:

export interface FastifyLoggingDecorators {
  log(message: string): void;
}

export const FASTIFY_LOGGING_DECORATOR_NAMES = ['log'];

const fastifyLoggingPlugin = fp(async (fastify: FastifyInstance) => {
  fastify.decorate('log', (message: string) => {
    // implement the logic
  });
});

export default fastifyLoggingPlugin;

An auth plugin that decorate request:

export interface RequestAuthDecorators {
  getAuthenticatedUser(): { id: string; name: string } | null;
}

export const REQUEST_AUTH_DECORATOR_NAMES = ['getAuthenticatedUser'];

const requestAuthPlugin = fp(async (fastify: FastifyInstance) => {
  fastify.decorateRequest('getAuthenticatedUser', function (this: FastifyRequest) {
    // Implement the logic here
    return null;
  });
});

export default requestAuthPlugin;

A static file handler plugin that decorate reply:

export interface ReplyStaticDecorators {
  sendFile(filename: string, rootPath?: string): FastifyReply;
}

export const REPLY_STATIC_DECORATOR_NAMES = ['sendFile'];

const replyFormatPlugin = fp(async (fastify: FastifyInstance) => {
  fastify.decorateReply('sendFile', function (this: FastifyReply, filename: string, rootPath: string = './') {
    // Implement the logic here
    return this;
  });
});

export default replyFormatPlugin;

Then, a plugin to configure routes:

const routePlugin = fp(async (fastify: FastifyInstance) => {
  // Get all the necessary dependencies with compilation/runtime check
  const { log } = fastify.getFastifyDecorators<FastifyLoggingDecorators>(FASTIFY_LOGGING_DECORATOR_NAMES);
  const { getAuthenticatedUser } = fastify.getRequestDecorators<RequestAuthDecorators>(REQUEST_AUTH_DECORATOR_NAMES);
  const { sendFile } = fastify.getReplyDecorators<ReplyStaticDecorators>(REPLY_STATIC_DECORATOR_NAMES);

  fastify.get('/user', (request, reply) => {
    const user = getAuthenticatedUser.call(request);
    if (!user) {
      log('Unauthenticated access attempt');
      return reply.send({ success: false, message: 'Unauthorized' });
    }

    log(`User profile accessed by ${user.id}`);
    return sendFile.call(reply, 'user-profile.html', '/views');
  });
});

export default routePlugin;

@metcoder95
Copy link
Copy Markdown
Member

Not to be a hard opposer of the feature, but I'm still having hard times seeing use cases for these new APIs.
Nevertheless I believe there's a use case that can be interesting and its to verify dependencies between plugins.

If e.g. there's plugin A that depends on B decorating the FastifyRequest object, plugin A can use fastify.getRequestDecorators to validate the decorator exists and throw or handle something different otherwise.

It might overlap already to what fp already offers; but we can make that, instead of overlapping, fp can rely on this for more strict checks.

Besides that, I'm having some troubles seeing more complex use case around it.

@jean-michelet
Copy link
Copy Markdown
Member Author

If the solution in this PR is right and applicable to all the core plugins, I might be happy enough to close this PR by myself.

Can you take a look?

@mcollina
Copy link
Copy Markdown
Member

My recommendation has not been implemented yet: #5768 (review).

I think we could have:

const { sendFile } = reply.getDecorator(..)

In this way: sendFile could be automatically bound to reply (use a proxy and lazily bind them on first access), completely avoiding call.

@mcollina
Copy link
Copy Markdown
Member

#5672 is likely going to be semver-major.

@jean-michelet
Copy link
Copy Markdown
Member Author

I've started looking into Node DI containers, and I think it's possible to create something really satisfying with Fastify for people who prefer this way of developing an application.

Closing for #6019.

@jean-michelet jean-michelet deleted the allow-access-decorators-before-boot branch March 15, 2025 22:18
@jean-michelet jean-michelet restored the allow-access-decorators-before-boot branch March 18, 2025 12:53
@jean-michelet jean-michelet reopened this Mar 18, 2025
@jean-michelet
Copy link
Copy Markdown
Member Author

jean-michelet commented Mar 18, 2025

Sorry, I reopen it 😛

Reading the comment of jsummers on the last mentioned issue, I think we still can allow access to decorators to simplify typing and dependency check, for business logic too. IoC container is an other topic.

No need for module augmentation here:

fastify.getDecorator<SomeFeature>('someFeature') // great for dependency check at registration
request.getDecorator<SomeFeature>('someFeature')
reply.getDecorator<SomeFeature>('someFeature')

I am gonna implement it the way Matteo recommend it.

@jean-michelet
Copy link
Copy Markdown
Member Author

jean-michelet commented Mar 21, 2025

I think, we should add a method Request.setDecorator only allowed when the application is ready for this kind of use case:

fastify.decorateRequest('sessionData', null)
fastify.addHook('onRequest', async (req, reply) => {
  req.setDecorator<ISession>('sessionData', { user: 'Alice' })
}

Cleaner than using as keyword, eg.:

(req as typeof req & { someProp: string }).someProp = 'hello'

@jean-michelet jean-michelet force-pushed the allow-access-decorators-before-boot branch from 67e6cb7 to b8e929e Compare March 22, 2025 11:12
@jean-michelet jean-michelet requested review from Eomm and mcollina and removed request for climba03003 and gurgunday March 22, 2025 11:15
@jean-michelet
Copy link
Copy Markdown
Member Author

jean-michelet commented Mar 22, 2025

In order to explain the evolution of my thinking, I initially wanted to propose something that would allow more control over the typing and runtime safety of both business logic and installed core plugins.

But I think it's possible to decouple core plugin utilities if desired by the users of the framework.

I've explained my reasoning a little further in the updated doc, I'll read your feedback carefully.

@jean-michelet jean-michelet changed the title feat: allow to access decorators before start. feat: allow to access decorators Mar 22, 2025
Copy link
Copy Markdown
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Copy link
Copy Markdown
Member

@Eomm Eomm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor comments, LGTM!

@Eomm Eomm mentioned this pull request Mar 28, 2025
@Eomm Eomm added the semver-minor Issue or PR that should land as semver minor label Mar 28, 2025
Copy link
Copy Markdown
Member

@gurgunday gurgunday left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work!

lgtm

@Eomm Eomm merged commit c2754bf into fastify:main Mar 29, 2025
38 of 41 checks passed
@jean-michelet jean-michelet deleted the allow-access-decorators-before-boot branch March 29, 2025 08:46
@climba03003 climba03003 mentioned this pull request Mar 19, 2026
@github-actions
Copy link
Copy Markdown

This pull request has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 25, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

documentation Improvements or additions to documentation semver-minor Issue or PR that should land as semver minor typescript TypeScript related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants