As an undergrad I was thought about ADT (Abstract Data Type) and then the "proper" way was to provide a _new method to allocate the data. This also allowed to completely hide the type as the C header didn't need to show the content of the struct being allocated.
If you go with not malloc'ing internally then you need to expose the entire struct and I tend to go about it by using an extra header X_internal.h that is explicitly showing the internals but expects you not to abuse this knowledge.
It's a tradeoff and I currently tend towards the second option more often than not.