TypeORM + GraphQL(Type-graphQL) bằng Typescript cho ứng dụng Nodejs
Chào mọi người
Bài viết này sẽ hướng dẫn các bạn tạo một dứng dụng Nodejs với TypeORM và GraphQL bằng Typescript
1. Giới thiệu
- Nodejs thì hiện khá phổ biến hiện nay, nó được xây dựng dựa trên JavaScript runtime. Trước khi bắt đầu mọi thứ thì bạn hãy download về và cài đặt vào máy nhé
- TypeORM là một ORM library, dùng để xây dựng các model trong cở sở dữ liệu cũng như việc tương tác với chúng
- Expressjs là một framework dùng để xây dựng ứng dụng Nodejs.
- Typescript là một phiên bản nâng cao của Javascript, nó bổ sung kiểu, lớp và hướng đối tượng giúp cho bạn code dễ hơn và tránh sai sót các lỗi về kiểu
- GraphQL là ngôn ngữ truy vấn các API. Nó cho phép phía client-side chỉ lấy các field cần thiết để xử lý trên Front-end
- TypeGraphQL Khi bạn sử dụng lib này, bạn không cần định nghĩa các schema theo cách thông thường như ở đây, chúng ta sẽ dùng các decorator trực tiếp trên model để GraphQL hiểu các schema của chúng ta
2. Yêu cầu hệ thống
- Node.js 12.0 trở lên
- MacOS, Windows (bao gồm WSL), và Linux cũng được hỗ trợ
3. Bắt đầu
3.1 Khởi tạo project
Sau khi cài đặt Nodejs, bạn tạo 1 folder rỗng và dùng command line gõ lệnh npm init
để tạo file tạo file package.json
3.2 Cấu hình Typescript
Đâu tiền bạn gõ các lệnh sau để cài đặt các package dùng để build Typescript
npm i --save-dev npm-run-all tsc tsc-watch rimraf
npm install typescript
Sau đó, bạn tạo file tsconfig.json
trong root folder với nội dung cấu hình như bên dưới
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"lib": [
"dom",
"es6",
] /* Specify library files to be included in the compilation. */,
"sourceMap": true /* Generates corresponding '.map' file. */,
"outDir": "dist" /* Redirect output structure to the directory. */,
"rootDir": "src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
/* Module Resolution Options */
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
"experimentalDecorators": true /* Enables experimental support for ES7 decorators. */,
"emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */,
"skipLibCheck": true,
"esModuleInterop": true
},
"compileOnSave": true,
"exclude": ["node_modules"],
"include": ["src/**/*.ts"]
}
- Property
include
chỉ đường dẫn các file ts sẽ được compile,outDir
là chỉ đường các file js được compile ra, bạn có thể sửa chổ này theo cách của bạn, còn không thì xài mặc định như mình. rootDir
chỉ ra root folder của tất cả files ts. Ở đây là thư mục src, vì thế bạn hãy tạo folder này trong thư mục root của bạncompileOnSave: true
sẽ tự động build lại các file ts của bạn sau khi save
Xem thêm tại https://www.typescriptlang.org/tsconfig
Bạn mở file package.json
, và update lại thuộc tính scripts như sau, Lưu ý là chỉ update chổ thuộc tính scripts thôi nha
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"clean": "rimraf dist/*",
"tsc": "tsc",
"dev": "npm-run-all build watch",
"build": "npm-run-all clean tsc",
"watch": "tsc-watch --onSuccess \"node dist/index\" --onFailure \"echo Beep! Compilation Failed\" --compiler typescript/bin/tsc"
}
3.3 Tạo Models bằng TypeORM
3.3.1 Cài đặt
Bạn gõ các lệnh sau để cài đặt các package liên quan
npm i mysql reflect-metadata typedi@^0.8.0 typeorm@^0.2.37 typeorm-typedi-extensions@^0.2.3
npm i --save-dev @types/mysql
3.3.2 Tạo models
Product: Bạn tạo file src/models/Product.ts
với nội dung như sau
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
Unique,
UpdateDateColumn,
ManyToOne,
JoinColumn,
} from "typeorm";
import { Category } from "./Category";
@Entity()
@Unique(["code"])
export class Product {
@PrimaryGeneratedColumn()
public readonly id!: number;
@Column({ type: "varchar" })
public code!: string;
@Column({ type: "varchar" })
public name!: string;
@CreateDateColumn()
public createdAt!: Date;
@UpdateDateColumn()
public updatedAt!: Date;
@ManyToOne((_type) => Category, (category: Category) => category.id)
@JoinColumn({ name: "categoryId" })
public category!: Category;
}
@Entity
báo cho TypeORM biết đây là 1 bảng trong db, có nhiều options trong decorator này nhưng mình chỉ sử dụng mặc định@PrimaryGeneratedColumn
khoá chính tự động tăng dần@Column
định nghĩa các thuộc tính của cột trong bảng@Unique
xác định cột code sẽ là có giá trị duy nhất trong toàn bộ db@ManyToOne
thể hiện mối quan hệ N-1 (nhiều Product thuộc một Category) với khoá ngoại làcategoryId
được định nghĩa bằng@JoinColumn
Category: Bạn tạo file src/models/Category.ts
với nội dung như sau
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
Unique,
UpdateDateColumn,
OneToMany,
} from "typeorm";
import { Product } from "./Product";
@Entity()
@Unique(["code"])
export class Category {
@PrimaryGeneratedColumn()
public readonly id!: number;
@Column({ type: "varchar" })
public code!: string;
@Column({ type: "varchar" })
public name!: string;
@CreateDateColumn()
public createdAt!: Date;
@UpdateDateColumn()
public updatedAt!: Date;
@OneToMany((_type) => Product, (product: Product) => product.category)
public products?: Product[];
}
- Các cú pháp tương tự Product nhé
@OneToMany
thể hiện mối quan hệ 1-N, nên property này có kiểuarray
Xem thêm tại https://typeorm.io/#/entities
3.3.3 Kết nối database
Bạn tao file .env
ở root folder có nội dung như bên nhưng nhớ thay thế giá trị theo thông tin CSDL trên máy bạn
DATABASE_NAME=my_db
DATABASE_HOST=localhost
DATABASE_PASSWORD=root
DATABASE_USER=root
DATABASE_PORT=3306
Sau đó, tạo file ormconfig.ts
ở root folder với nội dung như sau
module.exports = {
type: "mysql",
host: process.env.DATABASE_HOST,
port: process.env.DATABASE_PORT,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_NAME,
synchronize: true,
logger: "advanced-console",
logging: process.env.NODE_ENV === "production" ? ["error", "warn"] : "all",
cache: true,
dropSchema: false,
entities: ["dist/models/*.js"],
};
- Thuộc tính entities chỉ đường dẫn tới các file models đã được chuyển đổi sang js nên ở đây bạn thấy đường dẫn bắt đầu từ thư mục
dist
. Để thay đổi đường dẫn các file build js, thì bạn update lại thuộc tínhoutDir
trong filetsconfig.json
, còn không cứ xài mặc định như mình synchronize: true
khi bạn thêm/xoá thuộc tính trong class Product/Category, TypeORM sẽ modify các table trong db của bạn. Nên setfalse
khi deploy to production để tránh ảnh hưởng db
Xem thêm tại https://typeorm.io/#/using-ormconfig
3.3.4 Test db connection
Bạn tạo file src/index.ts
với nội dung như sau
import "reflect-metadata";
import { Container } from "typedi";
import * as TypeORM from "typeorm";
// register 3rd party IOC container
TypeORM.useContainer(Container);
const bootstrap = async () => {
try {
// create TypeORM connection
await TypeORM.createConnection();
} catch (err) {
console.error(err);
}
};
bootstrap();
Sau đó run npm run dev
để chạy ứng dụng. Mở db lên bạn sẽ thấy 2 bảng Product & Category xuất hiện
3.4 Cấu hình GraphQL
3.4.1 Cài đặt
Bạn gõ các lệnh sau để cài đặt các package liên quan
npm i express graphql type-graphql apollo-server-express@^2.9.16 class-validator
npm i --save-dev @types/express @types/graphql
3.4.2 Tạo lược đồ
Bạn update lại model Category như sau
import { Field, ObjectType } from "type-graphql";
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
Unique,
UpdateDateColumn,
OneToMany,
} from "typeorm";
import { Product } from "./Product";
@ObjectType()
@Entity()
@Unique(["code"])
export class Category {
@Field((_type) => Number)
@PrimaryGeneratedColumn()
public readonly id!: number;
@Field()
@Column({ type: "varchar" })
public code!: string;
@Field()
@Column({ type: "varchar" })
public name!: string;
@Field()
@CreateDateColumn()
public createdAt!: Date;
@Field()
@UpdateDateColumn()
public updatedAt!: Date;
@Field((_type) =>[Product])
@OneToMany((_type) => Product, (product: Product) => product.category)
public products?: Product[];
}
@ObjectType
xác định lược đồ GraphQL là class Product@Field
để khai báo các thuộc tính của class sẽ ánh xạ tới GraphQL. Ở thuộc tính products, mình định nghĩa kiểu là array[Product]
Các bạn làm tương tự cho model Product
. Ở thuộc tính category, mình sẽ định nghĩa là kiểy Object như sau @Field((_type) => Category)
*Rất gọn so với cách thông thường đúng không ^^! *
Xem thêm tại https://typegraphql.com/docs/types-and-fields.html
3.4.3 Tạo Repository
Mục đích của việc tạo Repository là để ta có thể sử dụng được các method của TypeORM và định nghĩa thêm nhiều method khác tuỳ vào mục đích sử dụng.
Bạn tạo một repository src/repositories/CategoryRepository.ts
với nội dung như sau
import { EntityRepository, Repository } from "typeorm";
import { Category } from "../models/Category";
@EntityRepository(Category)
export class CategoryRepository extends Repository<Category> {}
Cũng làm tương tự cho src/repositories/ProductRepository.ts
nhé
3.4.4 Tạo GraphQL modules
CreateCategory module
Đẻ tạo một module cho việc create 1 Category
, bạn cần tạo 1 file src/modules/category/CreateCategoryInput.ts
để định nghĩa dữ liệu đầu vào của GraphQL
import { Field, InputType } from "type-graphql";
import { Category } from "../../models/Category";
@InputType()
export class CreateCategoryInput implements Partial<Category> {
@Field()
public name!: string;
@Field()
public code!: string;
}
@InputType
sẽ sinh ra kiểuGraphQLInputType
Sau đó, bạn tạo một module src/modules/category/CreateCategory.ts
với nội dung như sau
import { Arg, Mutation, Resolver } from "type-graphql";
import { getCustomRepository } from "typeorm";
import { Category } from "../../models/Category";
import { CategoryRepository } from "../../repositories/CategoryRepository";
import { CreateCategoryInput } from "./CreateCategoryInput";
@Resolver((_type) => Category)
export class CreateCategory {
@Mutation((_type) => Category)
public async createCategory(
@Arg("data") inputData: CreateCategoryInput
): Promise<Category> {
const categoryRepository = getCustomRepository(CategoryRepository);
const category = categoryRepository.create({
name: inputData.name,
code: inputData.code,
});
await categoryRepository.save(category);
return category;
}
}
@Resolver
sẽ biến classCreateGroup
thành một REST API@Mutation
định nghĩa methodcreateCategory
là một GraphQL mutation@Arg
sẽ đưa giá trị phía client-side gửi lên vào paraminputData
getCustomRepository
Chúng ta sử dụng hàm này tạo 1 một thực thểcategoryRepository
và sử dụng các method của TypeORM. Đây là một cách đơn giản, mình sẽ hướng dẫn bạn một cách khác ở GetCategories module.
GetCategories module
Bạn tạo một module src/modules/category/GetCategories.ts
với nội dung như sau
import { Resolver, Query } from "type-graphql";
import { InjectRepository } from "typeorm-typedi-extensions";
import { Category } from "../../models/Category";
import { CategoryRepository } from "../../repositories/CategoryRepository";
@Resolver((_type) => Category)
export class GetCategories {
constructor(
@InjectRepository()
private readonly categoryRepository: CategoryRepository
) {}
@Query((_type) => [Category])
public async getCategories(): Promise<Category[]> {
const categories = await this.categoryRepository.find();
return categories;
}
}
InjectRepository
sẽ tự động tạo thực thểcategoryRepository
qua hàm xây dựng. Tuỳ mục đính chúng ta sẽ chọn cách nào phù hợp@Query
định nghĩa methodgetCategories
như là một GraphQL query
3.4.5 Tạo schema
Bạn tạo 1 file src/schema.ts
với nội dung như sau. Đây là cú pháp để build các modules của GraphQL, mỗi lần thêm một module mới bạn cần update lại file này và thêm nó vào thuộc tính resolvers
như [CreateCategory, GetCategories]
import { buildSchema } from "type-graphql";
import { CreateCategory } from "./modules/category/CreateCategory";
import { GetCategories } from "./modules/category/GetCategories";
export default (Container: any) => {
return buildSchema({
container: Container,
resolvers: [CreateCategory, GetCategories],
});
};
3.5 Calling GraphQL API
3.5.1 Tạo GraphQL server
Bạn update lại file src/index.ts
như sau
import "reflect-metadata";
import { Container } from "typedi";
import * as TypeORM from "typeorm";
import { ApolloServer } from "apollo-server-express";
import express from "express";
import cors from "cors";
import createSchema from "./schema";
// register 3rd party IOC container
TypeORM.useContainer(Container);
const bootstrap = async () => {
try {
// create TypeORM connection
await TypeORM.createConnection();
// build TypeGraphQL executable schema
const schema = await createSchema(Container);
const app = express();
const corsConfig = {
methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS",
credentials: true,
origin: [/localhost*/],
};
app.use(cors(corsConfig));
const port = 3000;
// Create GraphQL server
const server = new ApolloServer({
schema,
context: ({ req, res }) => ({ req, res }),
debug: true,
playground: true,
});
server.applyMiddleware({ app, cors: corsConfig });
app.listen({ port }, () => {
console.log(
`🚀 Server ready at http://localhost:${port}${server.graphqlPath}`
);
});
} catch (err) {
console.error(err);
}
};
bootstrap();
ApolloServer
tạo GraphQL Server,playground: true
để có thể test các schema trực tiếp tại localhost. Xem thêm options tại đâycorsConfig
Ở đây mình setorigin: [/localhost*/]
là để tất cả các app Frontend khác ở localhost có thể gọi được các API của app này, nếu không sẽ bị lỗi Cross-domain. Bạn thể setorigin: [*]
để bất kỳ một site nào cũng có thể gọi API từ app của bạn, xem thêm options tại cors-options
Sau đó, chạy lại ứng dụng bằng lệnh npm run dev
. Khi mà hình xuất hiện như bên dưới, nghĩa là bạn đã start GraphQL thành công
Sau đó các bạn truy cập vào http://localhost:3000/graphql sẽ được giao diện như bên dưới
Bạn click vào tab Schema hoặc Docs ở bên phải, sẽ hiển thị các Query, Mutation và kiểu dữ liệu của GraphQL. Chúng ta sẽ dùng nó để call các API mà không cần đọc lại code ^^!
3.5.2 Tạo Category
Bạn call createCategory module với câu truy vấn và data như sau, nhớ click qua tab Query Variables nhé
Lưu ý: Nếu bạn tạo trùng category code
thì TypeORM sẽ báo lỗi nhé, do trong model minh đã ràng buộc bằng @Unique
3.5.3 Lấy danh sách các categories
Giờ test thử xem data có lưu hay không bằng cách call GetCategories
query như bên dưới
Xem thêm cách call GraphQL API tại đây https://graphql.org/learn/queries/#gatsby-focus-wrapper
3.5.4 Lấy danh sách các categories với Product list
Bạn tự viết một module để tạo Product xem như luyện tập. Nhớ là phải set khoá ngoại categoryId
cho Product nhé
Sau khi tạo được một vài Product rồi, thi bạn update lại getCategories
method bằng cách thêm tuỳ chọn relations
. Nó được dùng dể query các Model có quan hệ với Model chính, ở đây là Category
@Query((_type) => [Category])
public async getCategories(): Promise<Category[]> {
const categories = await this.categoryRepository.find({
relations: ["products"],
});
return categories;
}
Sau đó, bạn update lại câu truy vấn như bên dưới để lấy được các Categories và Product của nó
Xem thêm tại https://typeorm.io/#/find-options
4. Source code
Đây là full source code sau khi làm xong các bước trên (dành cho những bạn nào muốn run trước learn sau 😄). Sau khi tải về bạn chỉ cần run 2 lệnh sau để cài đặt và chạy ứng dụng
npm i
npm run dev
5. Frontend App
Đây là bài viết hướng dẫn Tạo ứng dụng React bằng Gatsby + Apollo Client (GraphQL in Client), bạn có thể tham khảo và run ở local để call thử các API vừa tạo nhé ^^!
Enjoy!!
All Rights Reserved