A Date or Datetime is not text. The moment you want to put one into an email body, a CSV column, or an external API payload, you have to turn it into a String in a shape you control.
String.valueOf() turns a Date into the ISO shape yyyy-MM-dd, which is the format most systems expect:
Date launchDay = Date.newInstance(2024, 3, 15); System.debug(String.valueOf(launchDay)); // 2024-03-15
Datetime has a format() method that takes a pattern. Each letter stands for a part of the moment:
Datetime meeting = Datetime.newInstance(2024, 3, 15, 14, 5, 0);
System.debug(meeting.format('yyyy-MM-dd')); // 2024-03-15
System.debug(meeting.format('MMM d, yyyy')); // Mar 15, 2024
System.debug(meeting.format('HH:mm')); // 14:05
MM is the month as two digits, MMM is the short month name, dd is the day as two digits, HH is the hour on a 24 hour clock, and mm is the minute. Two digit patterns pad with a leading zero, which is why 5 minutes past came out as 05.
format() renders in the time zone of the running user, so two people can see two different strings for the same moment. formatGMT() takes the same pattern but always renders in GMT:
System.debug(meeting.formatGMT('yyyy-MM-dd HH:mm'));
Reach for formatGMT() for anything a machine will read, and plain format() for anything a person will read.
Date.valueOf() parses a String back into a Date:
Date parsed = Date.valueOf('2024-03-15');
The pattern is case sensitive. MM is the month but mm is the minute, and HH is a 24 hour clock while hh is a 12 hour clock. Mixing them up is the single most common formatting bug.
Integrations live and die on date formats. Sending 2024-03-15 when the other system wanted 03/15/2024 fails the whole payload, and knowing the pattern letters is what lets you match any spec you are handed.