String helper functions in NASM Win16 Assembly
There's plenty to optimize here!

In NASM you get the address simply by writing mov di, prompt. This has a shorter encoding than lea di, [prompt]. (In MASM this would be mov  di, offset prompt giving the same benefit over the lea form).

Instead of writing the pair mov ah, 0x4c mov al, 0, you could combine these in 1 instruction as mov ax, 0x4C00. This shaves off 1 byte from the program.

Your getchar returns a byte in AX and your putchar expects a byte in DX. You would be better off if you used AL and DL. This would avoid those several mov ah, 0 and mov dh, 0 instructions.

Your putchar code uses the BIOS.Teletype function 0x0E. This function does not expect anything in the CX register. What it does require is that you specify the displaypage in the BH register. Simply add mov bh, 0 here. And if it's even possible that your program has to run on the graphical video mode then it would make sense to write mov bx, 0x0007 because then the color for the character is taken from the BL register.

I see that the getstring code also checks for the linefeed code 10. No one does that. If the user presses the Enter key, you'll receive the carriage return code 13 and that's the only code that you need to check. The linefeed code only comes into play when outputting.

The pair of instructions mov [di], al inc di (3 bytes) can be replaced by the 1-byte instruction stosb. Given that your program is in the .COM file format we have DS=ES and the direction flag is almost certainly going to be clear. Ideal for using the string primitive assembly instructions. This also means that your putstring routine could use lodsb if you're willing to trade in DI for SI as the input parameter.

An interesting optimization comes from eliminating a tail call. You wrote call putchar directly followed by ret. This is equivalent to writing jmp putchar. Both shorter and faster this way!

Make it better


Your getstring procedure must not allow the user to input more than 19 characters. Anything more would overflow the 20-byte buffer.
Your getstring procedure should store (in the buffer) a terminating zero when the finishing Enter key arrives. This way the buffer can be used repeatedly and not just this one time.
In assembly we want to avoid all kinds of jumping because those are more time consuming than many other instructions.
Your putstring code uses a je and a jmp instruction on each iteration of the loop. The code below only uses the jne instruction on each iteration.

; IN (di)
putstring:
    jmp     .first
.continue:
    call    putchar
    inc     di           ; move to the next character
.first:
    mov     al, [di]     ; grab the next character of the string
    cmp     al, 0
    jne     .continue
    ret

; IN (al)
putchar:
    mov     ah, 0x0E     ; BIOS.Teletype
    mov     bx, 0x0007
    int     0x10
    ret


Using DX as the input for putchar is a poor choice, not only because DL would be enough, but especially because you need the character in AL anyway. So why not move it there in the first place?


Be consistent

Always write your numbers the same way. You wrote mov ah, 0x4c and also mov ah, 0x0E.
I suggest you use capitals for the hexadecimal digits and always write as many digits as will fit in the destination. So don't write stuff like mov ah, 0xE.
In case you're wondering why I make this suggestion. Using uppercase hexadecimal digits enhances the contrast with the lowercase 0x prefix or lowercase h suffix. Readability is very important in a program.

mov ah, 0x4C
mov ah, 0x0E


or

mov ah, 4Ch
mov ah, 0Eh


For many programmers function numbers are easiest recognized when expressed in hexadecimal.
You could thus write mov ah, 0x00 int 0x16 in your getchar routine.



As a final note, your labels are well chosen and the comments that you've added are all to the point. Congrats...