What is a right way to work with classes and OOP

I am working with classes right now and I got curious about the way the work with them should look like.

Maybe if you can recommend some good and descriptive books on the topic it would be great!

Questions:

  1. Now I am working on the backend and I am trying to use classes everywhere I find it appropriate – services, endpoints, others. I have some Angular experience and there I always provide new services or whatsoever it is via constructor making kinda injection into the class. Is that the pattern I should always inject the stuff into the class using the constructor?

  2. Let’s say I’ve got a very simple class imageService:

import { PutObjectRequest } from "@aws-sdk/client-s3"
import { Upload } from "@aws-sdk/lib-storage"
import { Types } from "mongoose"
import env from "../env"
import { logMethod } from "../helpers"
import { debug } from "../utils/debug"
import { S3Client } from '@aws-sdk/client-s3'

export interface ImageUploadParams {
    Body: PutObjectRequest["Body"],
    Key: string,
    ContentType: string,
    Bucket?: string,
    ACL?: string,
}

class ImageService {

    private readonly s3: S3Client

    constructor() {

        this.s3 = new S3Client({
            credentials: {
                accessKeyId: env.s3.accessKeyId,
                secretAccessKey: env.s3.secretAccessKey,
            },
            region: env.s3.region,
        })

    }

    private readonly allowedTypes: string[] = [
        'image/jpeg',
        'image/png',
    ]

    private generateName({ id, prefix }: { id: string | Types.ObjectId, prefix?: string }) {
        return `${prefix ? prefix + '-' : ''}image-${id}-${Date.now()}`
    }

    generate({ id, prefix }: { id: string | Types.ObjectId, prefix?: string }) {
        const name = this.generateName({ id, prefix })

        return {
            key: name,
            link: `https://${env.s3.bucket}.s3.${env.s3.region}.amazonaws.com/${name}`
        }
    }

    extractKey(link: string) {
        return link.split('/').pop()
    }

    checkType(mimetype: string) {
        return this.allowedTypes.includes(mimetype)
    }

    @logMethod
    async uploadImage({ Body, Key, ContentType, Bucket, ACL }: ImageUploadParams) {

        const upload = new Upload({
            client: this.s3,
            params: {
                Body,
                Key,
                ContentType,
                Bucket: Bucket ?? env.s3.bucket,
                ACL: ACL ?? 'public-read',
            },
        })

       if(!env.isProd) {
            upload.on("httpUploadProgress", (progress) => {
                if(progress && progress.loaded && progress.total) {
                    const percent = (progress.loaded / progress.total) * 100
                    debug(`image '${Key}' uploaded: ${percent}%`)
                }
            })
       }

        await upload.done()
    }
}

export const imageService = new ImageService()

As you can see I am always instantiating the class and exporting outside (making it available for others). Is it a good practice to use classes like this? Or there is some kind of other flows or technics to make classes available?

  1. Every method in a class should encapsulate some unit logic. What about the way of working with the class. Let’s say I am using the class above and I need to upload an image and I should interact with the class imageService making three different requests – imageService.generate, imageService.checkType, imageService.uploadImage, and so on, and putting all the things together in the class it requires all of that. Am I right? So the class provides the abstractions the little unit tools you can cooperate with and put up some own stuff, without anything concrete like the whole operation from a to z. Maybe there are some principals or something

  2. What about errors? Let’s say I am checking the image type and it is not valid. Should I throw an Error right away, or in this pattern I should just return the error to the class that invoked the method and it should decide what to do? If the last, there is a change of a bug or something. Do classes responsible for throwing errors, or just responding and delegating that on the classes that invoked them?

The questions must be very silly and vague, but any thoughts are welcomed