Skip to content
CodeSook
CodeSook

15. Employee Controller


Continue with Employee Controller

เราจะกลับมาทำงานของเราต่อ
ในส่วนของ Employee ยังเหลือส่วนของ Controller ที่ต้องทำ เพื่อให้ REST API ของเราทำงานได้ ต้องใช้ Controller นี่แหละ

File and Folder structure

src
├── configure
3 collapsed lines
│ └── openapi
│ ├── setup-openapi.ts
│ └── setup-scalar-docs.ts
├── controllers
│ ├── employees
│ │ ├── delete.ts
│ │ ├── get.ts
│ │ ├── index.ts
│ │ ├── post.ts
│ │ └── put.ts
│ ├── healthz.ts
├── index.ts
├── repositories
7 collapsed lines
│ ├── employees
│ │ ├── creates.ts
│ │ ├── finds.ts
│ │ ├── index.ts
│ │ ├── removes.ts
│ │ └── updates.ts
│ └── prisma.ts
├── schema
8 collapsed lines
│ ├── branded.ts
│ ├── employee.ts
│ ├── employeeWithRelations.ts
│ ├── general.ts
│ ├── helpers.ts
│ ├── index.ts
│ ├── overtime.ts
│ └── overtimeWithRelations.ts
├── services
6 collapsed lines
│ ├── employee
│ │ ├── create.ts
│ │ ├── finds.ts
│ │ ├── index.ts
│ │ ├── removes.ts
│ │ └── updates.ts
└── types
4 collapsed lines
├── repositories
│ ├── employee.ts
└── services
├── employee.ts

Setup function for Employee Controller

เราใช้ Dependency Injection เหมือนเดิม ก็เลยสร้าง function ขึ้นมาครอบไว้ก่อน

Terminal window
src
├── controllers
├── employees
├── delete.ts
├── get.ts
├── index.ts
├── post.ts
└── put.ts
src/controllers/employees/get.ts
src/controllers/employees/get.ts
export function setupEmployeeGetRoutes(employeeService: EmployeeService) {
const app = new Hono()
return app
}

Get Many Employees

เราจะสร้าง OpenAPI Docs กันก่อน
ทำไมต้องสร้างตรงนี้
คำตอบคือสร้างตรงไหนก็ได้ แต่ผมแค่อยากให้มันอยู่ใกล้ๆกันจะได้หาง่ายๆ

และส่วนที่สำคัญคือ Controller เป็นแค่ส่วนหน้าที่เอาไว้เข้าถึง Business logic ของเรา
ถ้าวันนึงเราเปลี่ยนจาก REST API ไปใช้ gRPC หรือ CLI นั่นทำให้ Controllers เหล่านี้มันไม่ได้ใช้ละ และส่งผลให้ ตัว OpenAPI Docs เหล่านี้มันก็ไม่ได้ถูกใช้งานไปด้วย

Request and Response Schema

เหมือนเดิม ผมจะเน้นให้ตกลงกันก่อนว่าจะรับส่งข้อมูลอย่างไร มีอะไรต้องส่งไป มีอะไรต้องส่งกลับ แล้วเดี๋ยวคน implement ต้องไปหาทางทำให้มันเป็นไปตามที่ตกลงกันไว้ ให้ได้

ที่ฝั่ง Frontend อยากได้ข้อมูลของ Employee ทุกคน แต่ไม่ได้อยากได้ deletedAt ซึ่งตอนเราเรียกใช้ employeeService มันจะต้องไม่เอา Employee ที่ถูก stamp deletedAt อยู่แล้ว
แต่เราจะได้ deletedAt: null มาน่ะสิ
เราต้องการให้มันหายไป ก็สร้าง Effect Schema ขึ้นมา โดยเอา Schema เดิมน่ะแหละ มาแก้ไข เอา deletedAt ออกไป แบบนี้

src/controllers/employees/get.ts
src/controllers/employees/get.ts
import * as S from "effect/Schema"
import { EmployeeWithRelationsSchema } from "../../schema/index.js"
const getManyResponseSchema = S.Array(EmployeeWithRelationsSchema.Schema.omit("deletedAt"))

Get Many Employees OpenAPI Doc

ต่อมาเราจะมาทำ document สำหรับ Controller Route ที่เอาไว้ดึงข้อมูล Employees
ตรงนี้เราก็จะบอกด้วยว่าจะ Response อะไรกลับไป โดยดึงเอา Effect Schema ที่ได้ทำไว้มาใช้เลย

src/controllers/employees/get.ts
src/controllers/employees/get.ts
const getManyResponseSchema = S.Array(EmployeeWithRelationsSchema.Schema.omit("deletedAt"))
const getManyDocs = describeRoute({
responses: {
200: {
content: {
"application/json": {
schema: resolver(getManyResponseSchema),
},
},
description: "Get Employees",
},
},
tags: ["Employee"],
})
export function setupEmployeeGetRoutes(employeeService: EmployeeService) {
const app = new Hono()
return app
}

ส่วนของ Request เราไม่ต้่องทำอะไร ก็เรียกมาเฉยๆ เราก็จะ Response ไปเลย
ตรง Request นี้ได้ทำแน่ๆ ตอนนี้เอาง่ายๆก่อน

ต่อมาเราก็จะทำ api controller กันแล้ว

src/controllers/employees/get.ts
src/controllers/employees/get.ts
const getManyResponseSchema = S.Array(EmployeeWithRelationsSchema.Schema.omit("deletedAt"))
const getManyDocs = describeRoute({
11 collapsed lines
responses: {
200: {
content: {
"application/json": {
schema: resolver(getManyResponseSchema),
},
},
description: "Get Employees",
},
},
tags: ["Employee"],
})
export function setupEmployeeGetRoutes(employeeService: EmployeeService) {
const app = new Hono()
app.get("/", getManyDocs, async (c) => {
const employees = await employeeService.findMany()
return c.json(Helpers.fromObjectToSchema(getManyResponseSchema)(employees), 200)
})
return app
}

ตรงนี้ก็ทำเหมือนเดิม new Hono()
จริงๆแล้วการ สร้าง Hono app อันใหม่เป็นวิธีการสร้าง Route Group ใน Hono อยู่แล้ว และในกรณีนี้เราก็ต้องการสร้าง Group ที่ชื่อว่า /employees

แล้วเราก็จะเอา function setupEmployeeGetRoutes() นี้ไปใช้ที่ index.ts

รวม Controller ทุกอันมาที่ index ก่อน

เราจะเอา Controller ที่เกี่ยวข้องกับ Employee มาไว้ที่ src/controllers/employees/index.ts ก่อนแล้วค่อยเอาไปเรียกใช้ที่ hono app ตัวหลักของเรา

คืออะไรที่จะอยู่ใน Group /employees ก็จะเอามาไว้ตรงนี้แหละ

import type { EmployeeService } from "../../types/services/employee.js"
import { Hono } from "hono"
import * as EmployeeGetRoutes from "./get.js"
export function setupEmployeeRoutes(employeeService: EmployeeService) {
const app = new Hono()
app.route("/", EmployeeGetRoutes.setupEmployeeGetRoutes(employeeService))
return app
}

จาก code ด้านบน เรามี function ที่เอาไว้ setup Employee group
ที่ต้องมี function ก็เพราะว่าเราจะทำ Dependency Injection

เราสร้าง new Hono() อีกแล้ว แล้วก็เอา controller ที่ทำ GET many Employee มาใส่

ตรงนี้เราก็ยังจะใช้ path ที่เป็น "/" แบบธรรมดานะ เพราะเดี๋ยวเราจะไปกำหนด group ให้มันที่ main Hono app

สุดท้ายก็ return hono app ด้วยนะ

add route group “/employees” to main Hono app

src/index.ts
src/index.ts
import * as EmployeeControllers from "./controllers/employees/index.js"
const app = new Hono()
5 collapsed lines
setupOpenApi(app)
app.route("/docs", setupScalarDocs())
app.route("/healthz", healthzApp)
const employeeRepository = initEmployeeRepository(prismaClient)
const employeeService = initEmployeeService(employeeRepository)
app.route("/employees", EmployeeControllers.setupEmployeeRoutes(employeeService))

จาก code ด้านบนเรากำหนด group ตรงนี้แหละ ก็ให้ใช้ "/employees"

ลองไปเปิด Scalar ดู จะได้แบบนี้

employe1

Get by EmployeeId

Response schema

เรามาตกลงกันก่อนว่าจะ response อะไรกลับไปให้ frontend หรือ client ที่จะเรียกมาที่ GET employee by EmployeeId

src/controllers/employees/get.ts
src/controllers/employees/get.ts
import * as S from "effect/Schema"
import { EmployeeWithRelationsSchema } from "../../schema/index.js"
const getByIdResponseSchema = EmployeeWithRelationsSchema.Schema.omit("deletedAt")

Open API Doc fro GET by EmployeeId

เราจะมาทำกันต่อ
อีก controller นึงจะเอาไว้ get employee by EmployeeId

มาทำ OpenAPI Doc กันก่อน

src/controllers/employees/get.ts
src/controllers/employees/get.ts
const getByIdResponseSchema = EmployeeWithRelationsSchema.Schema.omit("deletedAt")
const getByIdDocs = describeRoute({
responses: {
200: {
content: {
"application/json": {
schema: resolver(getByIdResponseSchema),
},
},
description: "Get Employee by EmployeeId",
},
},
tags: ["Employee"],
})

Validate Request

ตรงนี้จะทำเกี่ยวกับ Request ด้วยแล้ว

frontend หรือ client จะต้องส่ง EmployeeId มาด้วยในตอนที่ยิง Request มา
ในที่นี้เราจะกำหนดให้ส่งเป็น path params

เราจะเขียนแบบนี้

src/controllers/employees/get.ts
src/controllers/employees/get.ts
import { resolver, validator } from "hono-openapi/effect"
import { Branded, EmployeeWithRelationsSchema } from "../../schema/index.js"
const validateGetByIdRequest = validator("param", S.Struct({
employeeId: Branded.EmployeeIdFromString,
}))

จาก code ด้านบนเราจะใช้ validator() ที่ import มาจาก hono-openapi/effect
ใส่ arguments 2 ตัว

  1. จะให้ validate อะไร มี "param" | "json" | "cookie" | "header" | "form" | "query" Hono เรียกว่า Type ในทีนีเราเลือก "param"
  2. Effect Schema ที่จะใช้ validate ทุก Type จะใช้ Struct หมดเลยนะ

จะเห็นว่าเราใช้ Branded.EmployeeIdFromString เพราะว่า path params จะเป็น String เท่านั้น เหมือนกับ query ที่เป็น string เท่านั้นเช่นกัน

Let’s create Controller

src/controllers/employees/get.ts
src/controllers/employees/get.ts
import type { EmployeeService } from "../../types/services/employee.js"
import * as S from "effect/Schema"
import { Hono } from "hono"
import { describeRoute } from "hono-openapi"
import { resolver, validator } from "hono-openapi/effect"
3 collapsed lines
import { Branded, EmployeeWithRelationsSchema, Helpers } from "../../schema/index.js"
const getByIdResponseSchema = EmployeeWithRelationsSchema.Schema.omit("deletedAt")
const getByIdDocs = describeRoute({
responses: {
200: {
content: {
"application/json": {
schema: resolver(getByIdResponseSchema),
},
},
description: "Get Employee by EmployeeId",
},
},
tags: ["Employee"],
})
const validateGetByIdRequest = validator("param", S.Struct({
employeeId: Branded.EmployeeIdFromString,
}))
17 collapsed lines
const getManyResponseSchema = S.Array(EmployeeWithRelationsSchema.Schema.omit("deletedAt"))
const getManyDocs = describeRoute({
responses: {
200: {
content: {
"application/json": {
schema: resolver(getManyResponseSchema),
},
},
description: "Get Employees",
},
},
tags: ["Employee"],
})
export function setupEmployeeGetRoutes(employeeService: EmployeeService) {
const app = new Hono()
app.get("/:employeeId", getByIdDocs, validateGetByIdRequest, async (c) => {
const { employeeId } = c.req.valid("param")
const employee = await employeeService.findOneById(employeeId)
if (employee === null) {
return c.json({ message: `not found employee of id: ${employeeId}` }, 404)
}
const response = getByIdResponseSchema.make(employee)
return c.json(response, 200)
})
6 collapsed lines
app.get("/", getManyDocs, async (c) => {
const employees = await employeeService.findMany()
return c.json(Helpers.fromObjectToSchema(getManyResponseSchema)(employees), 200)
})
return app
}

จาก code ด้านบนจะเห็นว่า เอา OpenAPI Doc getByIdDocs มาใช้ได้ง่ายๆเลย เนื่องจากว่ามันเป็น Hono Middleware
ส่วน validateGetByIdRequest ก็เป็น Hono Middleware เหมือนกัน

ตอนที่เราจะดึง EmployeeId ออกมาจาก param ก็เรียกใช้แบบนี้ const { employeeId } = c.req.valid("param") ตรงนี้จะได้ type safe เลย Hono รู้เลยว่าจะมี employeeId แน่ๆ เพราะผ่าน validator มาแล้ว

ใน controller มีการเช็คก่อนด้วยว่าหา employee id นั้นๆเจอไหม
ถ้าไม่เจอ ก็จะให้ response 404 โดยส่งเป็น json {message: string}
แต่ในตัวอย่างไม่ได้ใส่ OpenAPI doc นะ ลองไปใส่เพิ่มกันดูได้

หลังจากเพิ่มมาแล้ว มันจะไปโผล่ที่ Scalar ด้วย

employee2