malloc vs realloc - what is the best practice?
Helper functions
When writing robust code, I avoid using library *alloc() functions directly.  Instead I form helper functions to handle various use cases and take care of edge cases, parameter validation, etc.
Within these helper functions, I use malloc(), realloc(), calloc() as building blocks, perhaps steered by implementation macros, to form good code per the use case.
This pushes the "what is best" to a narrower set of conditions where it can be better assessed - per function.  In the growing by 2x case, realloc() is fine.
Example:
// Optimize for a growing allocation
// Return new pointer.
// Allocate per 2x *nmemb * size.
// Update *nmemb_new as needed.
// A return of NULL implies failure, old not deallocated.
void *my_alloc_grow(void *ptr, size_t *nmemb, size_t size) {
  if (nmemb == NULL) {
    return NULL;
  }
  size_t nmemb_old = *nmemb;
  if (size == 0) { // Consider array elements of size 0 as error
    return NULL;
  }
  if (nmemb_old > SIZE_MAX/2/size)) {
    return NULL;
  }
  size_t nmemb_new = nmemb_old ? (nmemb_old * 2) : 1;
  unsigned char *ptr_new = realloc(ptr, nmemb_new * size);
  if (ptr_new == NULL) {
    return NULL;
  }
  // Maybe zero fill new memory portion.
  memset(ptr_new + nmemb_old * size, 0, (nmemb_new - nmemb_old) * size);
  *nmemb = nmemb_new;
  return ptr_new;
}
Other use cases.
/ General new memory
void *my_alloc(size_t *nmemb, size_t size); // General new memory
void *my_calloc(size_t *nmemb, size_t size); // General new memory with zeroing
// General reallocation, maybe more or less.
// Act like free() on nmemb_new == 0.
void *my_alloc_resize(void *ptr, size_t *nmemb, size_t nmemb_new, size_t size);