It is optional as far as the route and route matching is concerned. However, if you do not allow a null value for that parameter into your function, then the function is going to dictate that that particular value cannot be null. So, if that value does not exist, the route will match, but because the function is not setup to take a null (or optional value such as int id = 0), then it will fail with a null reference exception.
As to why passing it in as a class works, that is because the default value of an uninitialized integer is 0 (however, a int? default value is null). So, it has a value even though you never gave it one just from being "newed" up.