Trang chủ Kiến Thức Công Nghệ Hướng dẫn Upload Images lên S3 sử dụng Typescript
Công Nghệ

Hướng dẫn Upload Images lên S3 sử dụng Typescript

Chia sẻ
Hướng dẫn Upload Images lên S3 sử dụng Typescript
Chia sẻ

Cũng tương tự như bài Typescript Tutorial 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 để 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.

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

Sql

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

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

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é!

200Lab BlogViệt Trần

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 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.

Bash

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

Typescript

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

Bạn có thể dùng Docker để chạy container MySQL với câu lệnh:

Docker

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

Typescript

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

Typescript

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

Typescript

export class LocalDeleter {
  async deleteImageById(filename: string) {
    return true
  }
  cloudName() {
    return 'local'
  }
}
  • src/modules/images/infras/repository/delete/s3_deleter.ts

Typescript

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

Typescript

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

Typescript

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

Typescript

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

Typescript

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

Typescript

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

Typescript

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

Typescript

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

Typescript

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

Typescript

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

export { ErrImageNotFound }
  • src/modules/images/model/image.ts

Typescript

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

Typescript

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

Typescript

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

Typescript

export enum ImageStatus {
  UPLOADED = 'uploaded',
  USING = 'using',
  DELETED = 'deleted',
}
  • src/shared/error/index.ts

Typescript

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

Typescript

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é.

Env

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

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

  • 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
  • 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
  • DELETE /v1/images/:id
delete image s3

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
  • Hiểu về Module Alias trong Typescript. Tại sao phải dùng Alias?
  • Danh sách HTTP Status Code và Hướng dẫn sử dụng
  • Cách debugger trên 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
Chia sẻ

Để lại bình luận

Để lại một bình luận

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *

Bài viết cùng chuyên mục
Tối ưu ứng dụng với cấu trúc dữ liệu cơ bản và bitwise
Công Nghệ

Tối ưu ứng dụng với cấu trúc dữ liệu cơ bản và bitwise

Trong bài viết này, 200Lab sẽ chia sẻ những trường hợp dễ...

Công Nghệ

So sánh Flutter vs React Native: Framework nào đáng học năm 2021

Điểm chung của Flutter, React Native đều là Cross-platform Mobile, build native...

HTTP/2 là gì? So sánh HTTP/2 và HTTP/1
Công Nghệ

HTTP/2 là gì? So sánh HTTP/2 và HTTP/1

Từ khi Internet ra đời, sự phát triển về các giao thức...

Upload File từ Frontend đến Backend mà rất nhiều bạn vẫn đang làm sai!!
Công Nghệ

Upload File từ Frontend đến Backend mà rất nhiều bạn vẫn đang làm sai!!

1. Client encode file (base64) rồi gởi về backend 200Lab đã từng...

Công Nghệ

React Native – Hướng dẫn làm việc với Polyline và Animated-Polyline trên Map

Vẽ đường đi trên bản đồ là một nghiệp vụ vô cùng...

Công Nghệ

Hybrid App và Native App: Những khác biệt to lớn

Bất cứ khi nào một công ty quyết định làm ứng dụng...

Web/System Architecture 101 – Kiến trúc web/hệ thống cơ bản cho người mới
Công Nghệ

Web/System Architecture 101 – Kiến trúc web/hệ thống cơ bản cho người mới

Đây là một kiến trúc cơ bản mà bất kì một người...

Công Nghệ

Tư duy kiến trúc thông qua các trò chơi mà rất nhiều bạn không biết

Tư duy kiến trúc là gì? Tư duy kiến trúc có thể...

HTTP/3 là gì – Giao thức đột phá để tăng tải website
Công Nghệ

HTTP/3 là gì – Giao thức đột phá để tăng tải website

Nhắc lại một chút về HTTP/2 ở bài trước, từ khi giao...

Chiến lược leo lương: >= 1000$ NET (thậm chí > 3000$ NET)
Công Nghệ

Chiến lược leo lương: >= 1000$ NET (thậm chí > 3000$ NET)

Một ngàn đô Mỹ (1000$) là mức lương thường thấy cho vị...

Công Nghệ

Flutter vs React Native: Lựa chọn nào tốt nhất hiện nay

Flutter vs React Native? Có bao giờ bạn thắc mắc liệu sử...

Digital Ocean: Hướng dẫn tạo Droplet cùng 100$ FREE credit
Công Nghệ

Digital Ocean: Hướng dẫn tạo Droplet cùng 100$ FREE credit

Nếu bạn đang là người học lập trình, đặc biệt về backend,...