User Controllers, Services & File Upload (Day 4) - Creating a SaaS Startup in 30 Days

User Controllers, Services & File Upload (Day 4) - Creating a SaaS Startup in 30 Days

Exploring User Management, Services, and File Handling in Spring Boot for a SaaS Startup

May 10, 2024ยท

5 min read

Now that I have finished authentication on Spring Boot, I need to create the user table, controller, and services. Spring Boot has a very unique way of handling such tasks. I have to admit that it is a lot better than its competitors, and I really like the entire JVM ecosystem so far.

The only thing I dislike is that there is no hot-reload, and each time I have to rebuild the entire app. I think there has to be a tool or option, but I haven't figured it out yet.


The annotations are a game-changer, and I like them a lot. I don't need to include or import the Model anywhere; it just has to have the correct annotations, and Spring Boot finds it and creates the table. So, there is no need for migrations so far.

Write in the comments if you are using migrations with Spring Boot?

Also, data classes are a Kotlin feature. In regular Java, you had to add an annotation called @Data, but now we have that built into the language.

In addition as you can see in the second image the foreign keys are created automatically you just have to include OneOnOne annotation and the JoinColumn

@Table(name = "users")
data class UserModel(
    var id: String = UlidCreator.getUlid().toString(),
    var nickname: String = "",
    var firstname: String? = null,
    var lastname: String? = null,
    var userPassword: String? = null,
    var email: String? = null,
    var enabled: Boolean = true,
    var accountNonExpired: Boolean = true,
    var credentialsNonExpired: Boolean = true,
    var accountNonLocked: Boolean = true,

    @JoinColumn(name = "theme_id", referencedColumnName = "id")
    var theme: ThemeModel? = null,

    @JoinColumn(name = "avatar_media_id", referencedColumnName = "id")
    var avatar: MediaModel? = null,

    val role: RoleEnum = RoleEnum.ROLE_USER,

    ) : UserDetails {

    override fun getAuthorities(): Collection<GrantedAuthority> {
        return listOf(SimpleGrantedAuthority(

    override fun isEnabled(): Boolean {
        return enabled

    override fun isAccountNonExpired(): Boolean {
        return accountNonExpired

    override fun isCredentialsNonExpired(): Boolean {
        return credentialsNonExpired

    override fun isAccountNonLocked(): Boolean {
        return accountNonLocked

    override fun getUsername(): String? {
        return email

    override fun getPassword(): String? {
        return userPassword


Like models services are defines with the Service annotation and you dependency inject the classes that you want to use. I want to mention here that Kotlin has a really cool pattern.

The Kotlin code snippet { it } is a common pattern for handling optional (nullable) values. It utilizes Kotlin's safe call operator (?.) and the let function to safely work with a nullable value.

That way you can move away from if statements and you can use a lambda function with the let function to handle null values.

class UserService(
    private val userRepository: UserRepository,
    private val mediaService: MediaService,
    private val themeRepository: ThemeRepository
) {
    fun patch(id: String, request: UserRequest): UserResponse {
        val user = userRepository.findById(id).orElseThrow {
            throw IllegalStateException("User not found")
        } { = it }
        request.nickname?.let { user.nickname = it }
        request.firstname?.let { user.firstname = it }
        request.lastname?.let { user.lastname = it }
        request.themeId?.let {
            val theme = themeRepository.findById(it).orElseThrow {
                throw ResponseStatusException(HttpStatus.NOT_FOUND, "Theme not found")
            user.theme = theme

        return UserResponse(user)

    fun deleteAvatar(id: String): UserResponse {
        val user = userRepository.findById(id).orElseThrow {
            throw ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")

        user.avatar?.let { mediaService.deleteMedia( }
        user.avatar = null

        return UserResponse(user)

    fun uploadAvatar(id: String, file: MultipartFile): UserResponse {
        val user = userRepository.findById(id).orElseThrow {
            throw ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")

        user.avatar?.let { mediaService.deleteMedia( }

        val media = mediaService.saveMedia(file, "user")
        user.avatar = media

        return UserResponse(user)

    fun get(id: String): UserResponse {
        val user = userRepository.findById(id).orElseThrow() {
            throw ResponseStatusException(HttpStatus.NOT_FOUND, "User not found")

        return UserResponse(user)



Now that we have created our service, we can utilize it inside the UserController. As you can see in my code, we use annotations as expected, and we specify the paths as well. Also, I have created interfaces for the Request and the Response.

This is a really simple structure that is readable and self-documented.

class UserController(
    private val userService: UserService
) {
    fun patch(
        @RequestBody request: UserRequest,
        @PathVariable id: String,
    ): ResponseEntity<UserResponse> {
        return ResponseEntity.ok(userService.patch(id, request))

    fun uploadAvatar(
        @PathVariable id: String,
        @RequestBody avatar: MultipartFile,
    ): ResponseEntity<UserResponse> {
        return ResponseEntity.ok(userService.uploadAvatar(id, avatar))

    fun deleteAvatar(
        @PathVariable id: String,
    ): ResponseEntity<UserResponse> {
        return ResponseEntity.ok(userService.deleteAvatar(id))

    fun get(
        @PathVariable id: String,
    ): ResponseEntity<UserResponse> {
        return ResponseEntity.ok(userService.get(id))

File Upload Service

Here you can see on of the functions in my media service. I want you to take a look at 2 things. The first is how we handle exceptions it is really easy and works well.

fun saveMedia(file: MultipartFile, folder: String): MediaModel {
        val saveFolder = "$mediaFolder/$folder"
        val saveFolderFile = File(saveFolder)
        val fileExt = file.originalFilename?.substringAfterLast(".") ?: throw IllegalArgumentException("Invalid file name")
        val filename = generateMediaName(fileExt)
        if (!saveFolderFile.exists()) {
        val filePath = File(saveFolder, filename)

        file.inputStream.use { input ->
            filePath.outputStream().use { output ->

        val media = MediaModel(
            filename = filename,
            folder = folder,
            contentType = file.contentType ?: throw IllegalArgumentException("Invalid content type"),


Secondly, the way we handle the input stream with lambda functions is really unique. This is the same thing in Node.js, and as you can see, it is really complex. Even in python and PHP it is hard to do something similar.

function copyFile(inputPath, outputPath) {
    const inputStream = fs.createReadStream(inputPath);
    const outputStream = fs.createWriteStream(outputPath);


    // Optional: Handle stream events
    inputStream.on('error', (err) => {
        console.error('Error reading input file:', err);

    outputStream.on('error', (err) => {
        console.error('Error writing output file:', err);

    outputStream.on('finish', () => {
        console.log('File copied successfully.');

const inputPath = 'input.txt'; // Specify your input file path
const outputPath = path.join('output', 'output.txt'); // Specify your output file path

copyFile(inputPath, outputPath);


In conclusion, developing a SaaS startup with Spring Boot and Kotlin offers a robust and efficient way to handle user management and file uploads. The use of annotations and Kotlin's safe handling of nullable values simplifies the codebase while enhancing readability and maintainability.

Despite the lack of hot-reload in Spring Boot, its powerful ecosystem and straightforward approach to database modeling and service management make it a strong candidate for rapid development cycles.

As we continue to build out our startup, these tools will undoubtedly play a crucial role in our ability to deliver a scalable and user-friendly product.

Thanks for reading, and I hope you found this article helpful. If you have any questions, feel free to email me at, and I will respond.

You can also keep up with my latest updates by checking out my X here: