15. Employee Controller
Continue with Employee Controller
เราจะกลับมาทำงานของเราต่อ
ในส่วนของ Employee ยังเหลือส่วนของ Controller ที่ต้องทำ เพื่อให้ REST API ของเราทำงานได้ ต้องใช้ Controller นี่แหละ
File and Folder structure
src├── configure3 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├── repositories7 collapsed lines
│ ├── employees│ │ ├── creates.ts│ │ ├── finds.ts│ │ ├── index.ts│ │ ├── removes.ts│ │ └── updates.ts│ └── prisma.ts├── schema8 collapsed lines
│ ├── branded.ts│ ├── employee.ts│ ├── employeeWithRelations.ts│ ├── general.ts│ ├── helpers.ts│ ├── index.ts│ ├── overtime.ts│ └── overtimeWithRelations.ts├── services6 collapsed lines
│ ├── employee│ │ ├── create.ts│ │ ├── finds.ts│ │ ├── index.ts│ │ ├── removes.ts│ │ └── updates.ts└── types4 collapsed lines
├── repositories │ ├── employee.ts └── services ├── employee.ts
Setup function for Employee Controller
เราใช้ Dependency Injection เหมือนเดิม ก็เลยสร้าง function ขึ้นมาครอบไว้ก่อน
src├── controllers│ ├── employees│ │ ├── delete.ts│ │ ├── get.ts│ │ ├── index.ts│ │ ├── post.ts│ │ └── put.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
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
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
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
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 ดู จะได้แบบนี้

Get by EmployeeId
Response schema
เรามาตกลงกันก่อนว่าจะ response อะไรกลับไปให้ frontend หรือ client ที่จะเรียกมาที่ GET employee by EmployeeId
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
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
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 ตัว
- จะให้ validate อะไร มี
"param" | "json" | "cookie" | "header" | "form" | "query"
Hono เรียกว่า Type ในทีนีเราเลือก"param"
- Effect Schema ที่จะใช้ validate ทุก Type จะใช้ Struct หมดเลยนะ
จะเห็นว่าเราใช้ Branded.EmployeeIdFromString
เพราะว่า path params จะเป็น String เท่านั้น เหมือนกับ query ที่เป็น string เท่านั้นเช่นกัน
Let’s create Controller
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 ด้วย
