---
title: "Hướng dẫn Upload Images lên S3 sử dụng Typescript"
description: "Hướng dẫn upload images lên S3 sử dụng Typescript với 5 bước: Phân tích và Xây dựng User Story, Thiết kế cơ sở dữ liệu, Thiết kế REST API, Xây dựng REST API với Typescript, Sử dụng Postman để test API"
canonical: "https://200lab.io/blog/huong-dan-upload-images-s3-typescript"
published: "2024-08-27T01:33:11Z"
updated: "2026-08-21T07:17:53Z"
authors: ["Tran Thuy Vy"]
tags: ["Backend"]
reading_minutes: 14
access: "public"
---

Cũng tương tự như bài [Typescript Tutorial](</blog/lap-trinh-rest-api-todo-list-voi-typescript-express>) mình viết trước đó, các bạn nên thực hiện theo các bước như sau:

1. Phân tích và Xây dựng User Story.
2. Thiết kế cơ sở dữ liệu.
3. Thiết kế REST API cho Service.
4. Xây dựng REST API với Typescript.
5. Sử dụng [Postman](</blog/postman-la-gi>)để test API.

Bài viết này mình sẽ focus vào phần code typescript để upload image lên s3, bỏ qua bước 1 (Phân tích và Xây dựng User Story) mà bắt đầu từ bước số 2 luôn. Nếu các bạn chưa tự tin, cảm thấy khó khăn khi học các kiến thức nâng cao, đừng lo lắng 200Lab sẽ đồng hành cùng với bạn tại [khóa học Typescript](<https://200lab.io/khoa-hoc-lap-trinh-microservices-voi-typescript-va-express?ref=200lab.io>).

## 1. Thiết kế cơ sở dữ liệu

Đối với "Images" thì mình sẽ cần lưu trữ các thông tin: path, cloud, width, height, size, status, nên phần cơ sở dữ liệu cho Database MySQL mình sẽ thiết kế như sau:

- **Id** (Primary Key): định danh cho từng hình ảnh, mình sẽ sử dụng uuid v7 tương ứng với kiểu dữ liệu **varchar**.
- **Cloud**: nơi lưu trữ hình ảnh (**varchar**).
- **Width, Height, Size**: thông số của hình ảnh (**number**)
- **Status**: trạng thái của hình ảnh, vì có 3 giá trị: uploaded, using, deleted nên mình để kiểu **Enum, **về sau dễ dàng mở rộng hơn.
- **Created At**: thời gian hình ảnh được tạo trên hệ thống, đây là cột tuỳ chọn, mình thêm để tiện quản lý về sau.
- **Updated At**: thời gian hình ảnh được update lần cuối trên hệ thống, cũng là cột tuỳ chọn.

Đây là phần code tạo bảng trong MySQL

```
CREATE TABLE `image` (
  `id` varchar(36) NOT NULL,
  `path` TEXT NOT NULL,
  `cloud_name` varchar(100) CHARACTER SET utf8 NOT NULL,
  `width` int NOT NULL,
  `height` int NOT NULL,
  `size` int NOT NULL,
  `status` enum('Uploaded','Using', 'Deleted') DEFAULT 'Uploaded',
  `createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `updatedAt` timestamp NOT NULL ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```

Sau khi chạy lệnh bạn sẽ có được table như hình

![upload image sql](https://assets.200lab.io/images/2024/12/05/upload-image-sql-2-e17db13f6bd95cf5b70e.webp)

## 2. Thiết kế REST API cho Service

Đây sẽ là bước cực kỳ quan trọng, bạn có thể tham khảo thêm về cách thiết kế REST API ở bài viết bên dưới.

[REST API là gì? Cách thiết kế RESTful API bạn chưa biết](https://200lab.io/blog/rest-api-la-gi) — REST API là gì? Làm thế nào để thiết kế RESTful API hiệu quả? Cập nhật những thông tin mới nhất về REST API nhé!

Mình sẽ thiết kế REST API như sau:

- **POST /v1/images** tạo mới image với dữ liệu cần là `file` . Thuộc tính status mình sẽ để mặc định là **Uploaded**. API này mình sẽ trả về ID của image sau khi upload thành công. Duy nhất một ràng buộc là **cần phải upload ít nhất một file, không bỏ trống** là ổn.
- **GET /v1/images/:id** lấy toàn bộ thông tin chi tiết của một Image thông qua ID của nó.
- **DELETE /v1/images/:id** xoá một Image thông qua ID của nó. Trong ví dụ này, mình sẽ xoá luôn trong table, và xoá luôn trên [S3](</blog/amazon-s3-la-gi>) nhé. Trong thực tế, các bạn không nên xoá mà chỉ chuyển đổi trạng thái thành **deleted **như phần thiết kế cơ sở dữ liệu ban nãy mình đã đề cập.

## 3. Xây dựng REST API với Typescript

Phía trên là tất cả phần chuẩn bị của mình, bây giờ thì vào code thôi.

```
npm init -y
npm install uuid @types/uuid sequelize dotenv @aws-sdk/client-s3 @aws-sdk/lib-storage image-size express typescript ts-node mysql2 @types/node @types/express module-alias @types/module-alias cors @types/cors multer @types/multer --save-dev
npx tsc --init
```

Đây là nội dung file `tsconfig.json`

```
{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "baseUrl": "./src",
    "paths": {
      "@/*":["*"],
    },
  },
}

```

Bạn có thể dùng [Docker](</blog/docker-la-gi>) để chạy container MySQL với câu lệnh:

```
docker run -d --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=my-root-pass -e MYSQL_DATABASE=todo_db mysql:8.3.1
```

- `src/index.ts`

```
import 'module-alias/register';
import dotenv from 'dotenv';
import express, { type Express, type Request, type Response } from 'express';
import { sequelize } from './infras/sequelize';
import { initImages } from '@/modules/images/infras/repository/dto/image';
import { imageService } from '@/modules/images/module';

import cors from 'cors';

dotenv.config();

const app: Express = express();
const port = process.env.PORT || 8080;
app.use(cors());
(async () => {
  try {
    // check connection to database
    await sequelize.authenticate();
    console.log('Connection successfully.');
    initImages(sequelize);

    app.get('/', (req: Request, res: Response) => {
      res.send('200lab Server');
    });

    app.use(express.json());

    app.use('/v1', (req: Request, res: Response) =>
      imageService.setupRoutes(req, res)
    );

    app.listen(port, () => {
      console.log(`Server is running at http://localhost:${port}`);
    });
  } catch (error) {
    console.error('Unable to connect to the database:', error);
    process.exit(1);
  }
})();

```

- `src/infra/sequelize.ts`

```
import { Sequelize } from 'sequelize'
import { config } from 'dotenv'

config()

export const sequelize = new Sequelize({
  database: process.env.DB_NAME || '',
  username: process.env.DB_USERNAME || '',
  password: process.env.DB_PASSWORD || '',
  host: process.env.DB_HOST || '',
  port: parseInt(process.env.DB_PORT as string),
  dialect: process.env.DB_TYPE || 'mysql',
  pool: {
    max: 10,
    min: 0,
    acquire: 30000,
    idle: 10000
  },
  logging: false
})

```

- `src/modules/images/infras/repository/delete/local_deleter.ts`

```
export class LocalDeleter {
  async deleteImageById(filename: string) {
    return true
  }
  cloudName() {
    return 'local'
  }
}

```

- `src/modules/images/infras/repository/delete/s3_deleter.ts`

```
import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { IImageDeleter } from '@/modules/images/interfaces/usecase';
import { config } from 'dotenv';

config();

export class S3Deleter implements IImageDeleter {
  constructor() {}

  async deleteImage(filename: string): Promise<boolean> {
    const deleteParams = {
      Bucket: process.env.AWS_S3_BUCKET_NAME,
      Key: filename,
    };

    //delete file on s3
    try {
      s3.send(new DeleteObjectCommand(deleteParams));
    } catch (error) {
      console.error(error);
      return false;
    }

    return true;
  }

  cloudName(): string {
    return 'aws-s3';
  }
}

export const s3 = new S3Client({
  region: process.env.AWS_S3_REGION as string,
  credentials: {
    secretAccessKey: process.env.AWS_S3_SECRET_ACCESS_KEY as string,
    accessKeyId: process.env.AWS_S3_ACCESS_KEY_ID as string,
  },
});

```

- `src/modules/images/infras/repository/dto/image.ts`

```
import { DataTypes, Model, type Sequelize } from 'sequelize'
import { ImageStatus } from '@/shared/dto/status'

export class ImagePersistence extends Model {}

export function initImages(sequelize: Sequelize) {
  ImagePersistence.init(
    {
      id: {
        type: DataTypes.UUID,
        primaryKey: true,
        defaultValue: DataTypes.UUIDV4
      },

      path: {
        type: DataTypes.STRING,
        allowNull: false
      },

      cloudName: {
        type: DataTypes.STRING,
        allowNull: false,
        field: 'cloud_name'
      },

      width: {
        type: DataTypes.INTEGER,
        allowNull: true
      },

      height: {
        type: DataTypes.INTEGER,
        allowNull: true
      },

      size: {
        type: DataTypes.INTEGER,
        allowNull: false
      },

      status: {
        type: DataTypes.ENUM(ImageStatus.UPLOADED, ImageStatus.USING, ImageStatus.DELETED),
        allowNull: true
      }
    },
    {
      sequelize,
      modelName: 'Image',
      timestamps: true,
      tableName: 'images'
    }
  )
}

```

- `src/modules/images/infras/repository/uploader/local_uploader.ts`

```
import { IImageUploader } from '@/modules/images/interfaces/usecase';

export class LocalUploader implements IImageUploader {
  constructor() {}

  async uploadImage(
    filename: string,
    filesize: number,
    contentType: string
  ): Promise<boolean> {
    return true;
  }

  cloudName(): string {
    return 'local';
  }
}

```

- `src/modules/images/infras/repository/uploader/s3_uploader.ts`

```
import fs from 'fs';
import { config } from 'dotenv';
import { S3Client } from '@aws-sdk/client-s3';
import { Upload } from '@aws-sdk/lib-storage';
import { IImageUploader } from '@/modules/images/interfaces/usecase';

config();

export class S3Uploader implements IImageUploader {
  constructor() {}

  async uploadImage(
    filename: string,
    filesize: number,
    contentType: string
  ): Promise<boolean> {
    const parallelUploads3 = new Upload({
      client: s3,
      params: {
        Bucket: process.env.AWS_S3_BUCKET_NAME as string,
        Key: filename,
        Body: fs.readFileSync(filename),
        ContentType: contentType,
        ContentLength: filesize,
      },
      tags: [
        /*...*/
      ],
      queueSize: 4,
      partSize: 1024 * 1024 * 5,
      leavePartsOnError: false,
    });

    await parallelUploads3.done();
    return true;
  }

  cloudName(): string {
    return 'aws-s3';
  }
}

//set up new S3 client
export const s3 = new S3Client({
  region: process.env.AWS_S3_REGION as string,
  credentials: {
    secretAccessKey: process.env.AWS_S3_SECRET_ACCESS_KEY as string,
    accessKeyId: process.env.AWS_S3_ACCESS_KEY_ID as string,
  },
});

```

- `src/modules/images/infras/repository/mysql_image_repository.ts`

```
import { Sequelize } from 'sequelize';
import { IImageRepository } from '../../interfaces/repository';
import { Image } from '../../model/image';
import { ImagePersistence } from './dto/image';
import { ImageDetailDTO } from '../transport/dto/image_detail';

export class MySQLImagesRepository implements IImageRepository {
  constructor(readonly sequelize: Sequelize) {}

  async insertImage(data: Image): Promise<string> {
    try {
      const imageData = {
        id: data.id,
        path: data.path,
        cloudName: data.cloud_name,
        width: data.width,
        height: data.height,
        size: data.size,
      };

      const result = await ImagePersistence.create(imageData);

      return result.getDataValue('id');
    } catch (error: any) {
      throw new Error(`Error inserting image: ${error.message}`);
    }
  }

  async findById(id: string): Promise<ImageDetailDTO | null> {
    try {
      const image = await ImagePersistence.findByPk(id);

      return image ? image.get({ plain: true }) : null;
    } catch (error: any) {
      throw new Error(`Error finding image: ${error.message}`);
    }
  }

  async findByPath(path: string): Promise<ImageDetailDTO | null> {
    try {
      const image = await ImagePersistence.findOne({ where: { path } });

      return image ? image.get({ plain: true }) : null;
    } catch (error: any) {
      throw new Error(`Error finding image by path: ${error.message}`);
    }
  }

  async deleteImageById(id: string): Promise<boolean> {
    try {
      const image = await ImagePersistence.destroy({ where: { id } });
      return image ? true : false;
    } catch (error: any) {
      throw new Error(`Error deleting image: ${error.message}`);
    }
  }

  async updateStatus(id: string, status: string): Promise<boolean> {
    try {
      const image = await ImagePersistence.update(
        { status },
        { where: { id } }
      );
      return image ? true : false;
    } catch (error: any) {
      throw new Error(`Error updating image status: ${error.message}`);
    }
  }
}

```

- `src/modules/images/infras/transport/dto/image_detail.ts`

```
import { ImageStatus } from '@/shared/dto/status'

export class ImageDetailDTO {
  constructor(
    readonly id: string,
    readonly path: string,
    readonly cloudName: string,
    readonly width: number,
    readonly height: number,
    readonly size: number,
    readonly status: ImageStatus
  ) {}
}

```

- `src/modules/images/infras/transport/rest/routes.ts`

```
import { NextFunction, Router, type Request, type Response } from 'express';
import multer from 'multer';

import { IImageUseCase } from 'modules/images/interfaces/usecase';
import { ErrImageType } from 'shared/error';
import { ErrImageNotFound } from 'modules/images/model/image.error';
import { ensureDirectoryExistence } from 'shared/utils/fileUtils';

export class ImageService {
  constructor(readonly imageUseCase: IImageUseCase) {}

  async insert_image(req: Request, res: Response) {
    try {
      const file = req.file as Express.Multer.File;

      //check image type
      if (!file.mimetype.startsWith('image')) {
        res.status(400).send({ error: ErrImageType.message });
        return;
      }

      const imageId = await this.imageUseCase.uploadImage(
        file.destination + '/' + file.filename,
        file.size,
        file.mimetype
      );

      res.status(201).send({
        code: 201,
        message: imageId,
      });
    } catch (error: any) {
      res.status(400).send({ error: error.message });
    }
  }

  async detail_image(req: Request, res: Response) {
    try {
      const { id } = req.params;

      const image = await this.imageUseCase.detailImage(id);

      if (!image) {
        return res.status(404).json({ code: 404, message: ErrImageNotFound });
      }

      const fullPath = process.env.URL_PUBLIC + '/' + image.path;

      return res.status(200).json({
        code: 200,
        message: 'image',
        data: { ...image, path: fullPath },
      });
    } catch (error: any) {
      return res.status(400).json({ error: error.message });
    }
  }

  async delete_image(req: Request, res: Response) {
    try {
      const { id } = req.params;

      const image = await this.imageUseCase.detailImage(id);

      if (!image) {
        return res.status(404).json({ code: 404, message: ErrImageNotFound });
      }

      try {
        await this.imageUseCase.deleteImage(image.path);
        return res
          .status(200)
          .json({ code: 200, message: 'delete image successful' });
      } catch (error: any) {
        return res.status(400).json({ error: error.message });
      }
    } catch (error: any) {
      res.status(400).send({ error: error.message });
    }
  }

  async upload_images(req: Request, res: Response) {
    try {
      const files = req.files as Express.Multer.File[];

      const imageIds = await this.imageUseCase.uploadImages(files);

      res.status(201).send({
        code: 201,
        message: imageIds,
      });
    } catch (error: any) {
      res.status(400).send({ error: error.message });
    }
  }

  setupRoutes(req: Request, res: Response): Router {
    const router = Router();

    //multer
    const storage = multer.diskStorage({
      destination: function (req, file, cb) {
        const uploadPath = process.env.UPLOAD_PATH || 'uploads';
        ensureDirectoryExistence(uploadPath);
        cb(null, uploadPath);
      },
      filename: function (req, file, cb) {
        const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
        cb(null, uniqueSuffix + '-' + file.originalname);
      },
    });

    const upload = multer({
      storage: storage,
      fileFilter: (
        req: Request,
        file: Express.Multer.File,
        cb: multer.FileFilterCallback
      ) => {
        if (file.mimetype.startsWith('image/')) {
          cb(null, true);
        } else {
          cb(null, false);
        }
      },
    });

    router.post('/images', upload.single('image'), this.insert_image.bind(this));

    router.post(
      '/images/multi',
      upload.array('image', 5),
      this.upload_images.bind(this)
    );

    router.get('/images/:id', this.detail_image.bind(this));

    router.delete('/images/:id', this.delete_image.bind(this));

    return router;
  }
}

```

- `src/modules/images/interfaces/repository.ts`

```
import { ImageDetailDTO } from '../infras/transport/dto/image_detail';
import { Image } from '../model/image';

export interface IImageRepository {
  insertImage(image: Image): Promise<string>;

  findById(id: string): Promise<ImageDetailDTO | null>;

  findByPath(path: string): Promise<ImageDetailDTO | null>;

  deleteImageById(id: string): Promise<boolean>;

  updateStatus(id: string, status: string): Promise<boolean>;
}

```

- `src/modules/images/interfaces/usecase.ts`

```
import { ImageDetailDTO } from '../infras/transport/dto/image_detail';

export interface IImageUseCase {
  uploadImage(
    filename: string,
    filesize: number,
    contentType: string
  ): Promise<string>;

  uploadImages(files: Express.Multer.File[]): Promise<string[]>;

  detailImage(id: string): Promise<ImageDetailDTO | null>;

  deleteImage(filename: string): Promise<boolean>;

  changeStatus(id: string, status: string): Promise<boolean>;
}

export interface IImageUploader {
  uploadImage(
    filename: string,
    filesize: number,
    contentType: string
  ): Promise<boolean>;
  cloudName(): string;
}

export interface IImageDeleter {
  deleteImage(filename: string): Promise<boolean>;
  cloudName(): string;
}

```

- `src/modules/images/model/image.error.ts`

```
const ErrImageNotFound = new Error('Image Not Found')

export { ErrImageNotFound }

```

- `src/modules/images/model/image.ts`

```
import type { ImageStatus } from '@/shared/dto/status'

export class Image {
  constructor(
    readonly id: string,
    readonly path: string,
    readonly cloudName: string,
    readonly width: number,
    readonly height: number,
    readonly size: number,
    readonly status: ImageStatus
  ) {}
}

```

- `src/modules/images/usecase/image_usecase.ts`

```
import { v7 as uuidv7 } from 'uuid';
import {
  IImageDeleter,
  IImageUploader,
  IImageUseCase,
} from '../interfaces/usecase';
import { IImageRepository } from '../interfaces/repository';
import { ImageStatus } from 'shared/dto/status';
import { ImageDetailDTO } from '../infras/transport/dto/image_detail';
import { ErrImageNotFound } from '../model/image.error';
import sizeOf from 'image-size';
import fs from 'fs';

export class ImageUseCase implements IImageUseCase {
  constructor(
    readonly imageRepository: IImageRepository,
    readonly imageUploader: IImageUploader,
    readonly imageDeleter: IImageDeleter
  ) {}

  async uploadImage(
    filename: string,
    filesize: number,
    contentType: string
  ): Promise<string> {
    const dimensions = sizeOf(filename);

    this.imageUploader.uploadImage(filename, filesize, contentType);

    const imageId = uuidv7();

    const uploadImages = {
      id: imageId,
      path: filename,
      cloud: this.imageUploader.cloudName(),
      width: dimensions.width as number,
      height: dimensions.height as number,
      size: filesize,
      status: ImageStatus.UPLOADED,
    };

    await this.imageRepository.insertImage(uploadImages);

    if (this.imageUploader.cloudName() !== 'local') {
      //xóa file
      fs.unlink(filename, (err) => {
        if (err) {
          console.error(err);
          return;
        }
      });
    }

    return imageId;
  }

  async detailImage(id: string): Promise<ImageDetailDTO | null> {
    try {
      return await this.imageRepository.findById(id);
    } catch (error: any) {
      throw ErrImageNotFound;
    }
  }

  async deleteImage(filename: string): Promise<boolean> {
    try {
      const image = await this.imageRepository.findByPath(filename);
      if (!image) {
        throw ErrImageNotFound;
      }

      if (this.imageDeleter.cloudName() !== 'local') {
        this.imageDeleter.deleteImage(filename);
      } else {
        fs.unlink(filename, (err) => {
          if (err) {
            console.error(err);
            return false;
          }
        });
      }

      await this.imageRepository.deleteImageById(image.id);

      return true;
    } catch (error: any) {
      throw new Error(error.message);
    }
  }

  async changeStatus(id: string, status: string): Promise<boolean> {
    try {
      return await this.imageRepository.updateStatus(id, status);
    } catch (error: any) {
      throw new Error(error.message);
    }
  }

  async uploadImages(files: Express.Multer.File[]): Promise<string[]> {
    const uploadPromises = files.map((file) =>
      this.uploadImage(
        file.destination + '/' + file.filename,
        file.size,
        file.mimetype
      )
    );

    return Promise.all(uploadPromises);
  }
}

```

- `src/modules/images/module.ts`

```
import { sequelize } from 'infras/sequelize';
import { MySQLImagesRepository } from './infras/repository/mysql_image_repository';
import { ImageService } from './infras/transport/rest/routes';
import { ImageUseCase } from './usecase/image_usecase';
import { S3Uploader } from './infras/repository/uploader/s3_uploader';
import { S3Deleter } from './infras/repository/delete/s3_deleter';

export const imageService = new ImageService(
  new ImageUseCase(
    new MySQLImagesRepository(sequelize),
    new S3Uploader(),
    new S3Deleter()
  )
);

```

- `src/shared/dto/status.ts`

```
export enum ImageStatus {
  UPLOADED = 'uploaded',
  USING = 'using',
  DELETED = 'deleted',
}

```

- `src/shared/error/index.ts`

```
const ErrImageStatusPattern = new Error(
  'Image status must be UPLOADED, USING, DELETED'
);
const ErrImageType = new Error('Invalid image type');

const ErrSystem = new Error('System error');

const ErrCloudNameEmpty = new Error('Cloud name is required');

export { ErrImageStatusPattern, ErrImageType, ErrSystem, ErrCloudNameEmpty };

```

- `src/shared/utils/fileUtils.ts`

```
import fs from 'fs'

export const ensureDirectoryExistence = (dirPath: string): void => {
  if (!fs.existsSync(dirPath)) {
    fs.mkdirSync(dirPath, { recursive: true })
  }
}

```

- `.env`: Bạn điền các trường tương ứng với hệ thống của mình nhé.

```
DB_HOST=''
DB_USERNAME=''
DB_PASSWORD=''
DB_NAME=''
DB_TYPE=''
PORT=''
AWS_S3_ACCESS_KEY_ID=''
AWS_S3_SECRET_ACCESS_KEY=''
AWS_S3_REGION=''
AWS_S3_BUCKET_NAME=''
URL_PUBLIC=''
UPLOAD_PATH=''
```

## 4. Sử dụng Postman test API

- `POST /v1/images`: Bạn chỉ upload mỗi lần 1 hình ảnh duy nhất.

![api upload image s3](https://assets.200lab.io/images/2024/12/05/api-upload-image-6f3e58e6734b7c4d9aab.webp)

Sau khi upload image thành công thì database và trên s3 sẽ như hình bên dưới

![](https://assets.200lab.io/images/2024/12/05/upload-image-mysql-fb7a71ff8987a139f3bc.webp)
![](https://assets.200lab.io/images/2024/12/05/upload-image-s3-1d2005eb683325cdec6f.webp)

- `POST /v1/images/multi`: Cho phép bạn upload nhiều hình ảnh cùng một lúc.

![Upload Images S3 Typescript](https://assets.200lab.io/images/2024/12/05/upload-images-s3-f1c8ab230b51ab026fbb.webp)

- `GET: /v1/images/:id`: Lấy thông tin chi tiết hình ảnh (lúc này khi trả về bạn sẽ gắn URL\_PUBLIC vào trước path hình ảnh để trở thành link image sử dụng trong dự án).

![Get image detail](https://assets.200lab.io/images/2024/12/05/get-image-detail-s3-8488201cdf02d12587bc.webp)

- DELETE /v1/images/:id

![delete image s3](https://assets.200lab.io/images/2024/12/05/delete-image-s3-cdad981d6d4fda822f89.webp)

## 5. Kết luận

Kết thúc bài viết này, hy vọng rằng bạn đã cảm thấy việc xây dựng REST API với upload image, storing in S3 bằng Typescript không còn khó khăn với bạn.

Một vài chủ đề khác, bạn có thể sẽ hứng thú tại 200Lab:

- [Hướng dẫn Typescript Syntax cơ bản cho Người mới - Phần 1](</blog/typescript-syntax-co-ban-phan-1>)
- [Hiểu về Module Alias trong Typescript. Tại sao phải dùng Alias?](</blog/module-alias-trong-typescript>)
- [Danh sách HTTP Status Code và Hướng dẫn sử dụng](</blog/http-status-code>)
- [Cách debugger trên VSCode cho Typescript](</blog/cach-debugger-tren-vscode-cho-typescript>)
- [SOLID là gì? Lập trình Hướng đối tượng hiệu quả hơn với Nguyên lý SOLID](</blog/solid-la-gi>)
