I decided, rather than having a remote event corresponding to each spell/tool, I will deduplicate common logic into one centralized tool registry that looks up the correct function depending on which tool the player is holding. This removes boilerplate and makes the code more secure, since users can only use tools that they actually possess, since the server looks up the tool they are holding by name.
Similarly, I make the client have a centralized registry of visual effects and the server FireAllClients with the name of the visual effect used. Currently I use one module script containing the registry which is required and used by multiple RunContext = Client scripts, one for each visual effect. Each script would simply put functions into the table returned by the module script.
But is that good practice? If module scripts ever change to return a copy of the table instead of the original, this system will break.