How to structure an application, and driver code for UART - the concept of decoupling the data
Normally you would have something like stm32_usart.h + stm32_usart.c making up the actual driver. Normally there is no need to have a HAL in between that driver and the application, because UART is pretty straight-forward and simple. 

The only reason why you'd want a HAL between the application code and the driver is if the application is some sort of serial library: suppose you are implementing a printf-like function, an UART error logger or bootloader etc, something that you want to re-use in multiple projects. If so, it would make sense to wrap the UART in an additional HAL so that your serial library code becomes portable between different microcontrollers. That's where "opaque types", function pointers and all that comes in - only then.

If you have no such special requirements, then the additional HAL is just bloat and you should implement the simplest form of a UART driver:

// stm32_usart.h
#ifndef STM32_USART_H    
#define STM32_USART_H

typedef enum  // some error handling type for this driver
{
  USART_OK,
  USART_ERR_OVERRUN,
  USART_ERR_FRAMING,
  USART_ERR_INVALID_BAUD,
  ...
} usart_result_t;


usart_result_t  usart_init (uint32_t baudrate_hz, ... 
/* pick parameters to suit whatever functionality you wish the driver to include here, 
   handshaking, partity, stop bits etc */

usart_result_t usart_send (const uint8_t* data, size_t size);

usart_result_t usart_receive (uint8_t* data, size_t buf_size, size_t* data_received);

#endif // STM32_USART_H


And that's pretty much it. stm32_usart.c implements these functions. The simplest form of calling code might look something like:

usart_result_t  result;
result = usart_init(9600, N, 1);
if(result != USART_OK)
{
  /* handle errors */
}
...

do
  result = usart_send(data, size);
while(result == USART_BUSY);

if(result != USART_OK)
{
  /* handle errors */
}


And so on, this for the simplest form of busy-wait polling.

You can expand the functionality as needed: perhaps you need an rx interrupt. If so, that one should be in hidden inside the .c file and usart_receive should be the one to communicate with the ISR. 

(Though if you need to add the interrupt to a vector table written in C, you may have to expose the function prototype of the ISR in the header, so that the vector table can use the ISR name. This is highly MCU-specific but probably not needed in most Cortex M implementations.)

If you need such an interrupt, you'll likely either need a ring buffer ADT or some DMA library (always go with DMA if available, they tend to be painful to set up, but they do wonders for real-time performance). Meaning that stm32_usart.h should include the relevant library for that, but the library calling code will be placed inside the driver. 

I always place includes in the header rather than in the .c file, in order to document code dependencies to the one using your code. Nothing is more annoying that grabbing some 3rd party code and getting compiler errors for "missing function blabla". If you have hidden away your code dependencies for the application programmer, then are making it painful for them to trouble-shoot such errors.