There are a number of methods which can improve the quality of shadows. Once of which is CSM.
The first step is to constuct a fustrum using world space by multiplying the inverse of projection and view matrix to the NDC corners (these are in range of -1 and 1). The centre is calculated by averaging out the 4 corners. This look at matrix needs to be defined like using Centre.Add(LightDirection) and Centre. The fustrum corners then need to be transformed into light space to find minimum and maximum coordiantes.
The final thing is to increase size by using a ZMultipier which pushes back the far plane and pulls in the near plane. From here, I wrote a geometry shader which actually helps output the depth values directly to the textures depending on the level of near and far. To store the light matrices, I use a uniform buffer object (UBO) and pass this into my shaders.
The actual code within the shaders to check for shadows is fairly simple - essentially I calculate the cascade level to use depending on the depth value of the fragment position. From here it's very similar to simple shadow mapping.