สร้าง Device Management API ด้วย Go Fiber
Branch:
workshop/dev-03-device-apiPhase: Development — API Layer Repository: https://github.com/kangana1024/iot-workshop
เกริ่นก่อนลุย (╯°□°)╯
เคยไหมครับ เวลาเราต่อ IoT device เข้ามาในระบบ แล้วไม่รู้ว่ามันยังอยู่ไหม เปิดอยู่ไหม หรือพังไปแล้ว? วันนี้พี่โชว์จะพาน้องๆ มาสร้าง Device Management API ที่จัดการ device ได้ครบทุก operation ตั้งแต่ลงทะเบียนไปจนถึงสั่งงานจากระยะไกล
เหมือนเราทำระบบ “ทะเบียนบ้าน” ให้ IoT device นั่นแหละครับ — ลงทะเบียน, ดูสถานะ, แก้ข้อมูล, หรือจะลบออกก็ได้ มาลุยกัน!
น้องๆ จะได้เรียนรู้อะไรบ้าง
- ทำไมถึงต้องมี Device Management API (WHY ก่อน HOW เสมอ!)
- สร้าง CRUD Handler ครบชุดด้วย Go Fiber
- ทำ Pagination + Filtering + Sorting แบบ flexible
- Device Registration Flow ที่ออก Auth Token ตอน register เท่านั้น
- Bulk Operations — จัดการ device หลายตัวพร้อมกัน
- Status Monitor ทำงาน background ตรวจจับ device ที่หายไป
- ตั้งค่า Swagger Documentation อัตโนมัติ
ทำไมต้องมี Device Management API?
ลองนึกภาพนะครับ ถ้าเรามี sensor อยู่ 500 ตัวในโรงงาน แล้วอยากรู้ว่าตัวไหน offline อยู่บ้าง — จะไปเดินตรวจทีละตัวคงไม่ไหว ระบบ API ตรงนี้เลยทำหน้าที่เป็น “แผนกทะเบียน” ของ device ทั้งหมด
เปรียบเหมือนระบบ HR ของบริษัท:
POST /devices= สมัครงาน (register device ใหม่)GET /devices= ดูรายชื่อพนักงาน (list ทั้งหมด)PUT /devices/:id= แก้ข้อมูลพนักงาน (update)DELETE /devices/:id= ออกจากงาน แต่ยังเก็บ record ไว้ (soft delete)POST /devices/:id/commands= ส่ง email สั่งงาน (send command ผ่าน MQTT)
ภาพรวม REST API Flow
ก่อนเขียน code เรามาดู flow รวมกันก่อนนะครับ — WHY ก่อน HOW เสมอ!
เห็นไหมครับ ทุก request ผ่าน Handler → Usecase → MongoDB เป็น layered architecture เหมือนแฮมเบอร์เกอร์ — แต่ละชั้นมีหน้าที่ของตัวเองชัดเจน
API Endpoints ทั้งหมด
| Method | Path | ทำอะไร |
|---|---|---|
| GET | /api/v1/devices | List devices พร้อม pagination |
| POST | /api/v1/devices | Register device ใหม่ |
| GET | /api/v1/devices/:id | ดู device ตาม ID |
| PUT | /api/v1/devices/:id | Update device |
| DELETE | /api/v1/devices/:id | Soft delete device |
| POST | /api/v1/devices/:id/commands | ส่ง command ไปยัง device |
| GET | /api/v1/devices/:id/status | ดูสถานะ device |
| POST | /api/v1/devices/bulk | Bulk operations หลาย device พร้อมกัน |
Device Handler — หัวใจของทั้งหมด
internal/handler/device_handler.go
Handler คือ “พนักงานต้อนรับ” ครับ รับ HTTP request มา, ตรวจสอบข้อมูล, แล้วส่งต่อให้ Usecase ไปจัดการจริงๆ
package handler
import (
"github.com/gofiber/fiber/v2"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
"github.com/kangana1024/iot-workshop/backend/internal/domain"
"github.com/kangana1024/iot-workshop/backend/internal/usecase"
"github.com/kangana1024/iot-workshop/backend/pkg/response"
"github.com/kangana1024/iot-workshop/backend/pkg/validator"
)
// DeviceHandler handles HTTP requests for device management
type DeviceHandler struct {
deviceUsecase *usecase.DeviceUsecase
validator *validator.Validator
log *zap.Logger
}
// NewDeviceHandler creates a new DeviceHandler
func NewDeviceHandler(du *usecase.DeviceUsecase, v *validator.Validator, log *zap.Logger) *DeviceHandler {
return &DeviceHandler{
deviceUsecase: du,
validator: v,
log: log,
}
}
เราใช้ dependency injection ครับ — แทนที่จะสร้าง usecase ข้างใน, เราส่งเข้ามาจากข้างนอก เหมือนร้านอาหารที่ไม่ได้เลี้ยงวัวเอง แต่สั่งเนื้อจากซัพพลายเออร์ แยกหน้าที่กันชัดเจน
List Devices — ดึงรายการพร้อม Pagination
ทำไมต้อง pagination? ลองนึกดูว่าถ้า device 10,000 ตัว แล้วดึงทีเดียวหมด — server พังแน่ครับ เหมือนห้องสมุดที่ต้องค่อยๆ หยิบทีละหน้าจากชั้น ไม่ใช่ยกทั้งชั้นมาพร้อมกัน
// List godoc
// @Summary List devices
// @Description Returns a paginated list of devices with optional filters
// @Tags devices
// @Accept json
// @Produce json
// @Param page query int false "Page number (default: 1)"
// @Param limit query int false "Items per page (default: 20, max: 100)"
// @Param status query string false "Filter by status: online|offline|inactive|error"
// @Param type query string false "Filter by type: sensor|actuator|gateway|camera"
// @Param search query string false "Full-text search"
// @Param sort_by query string false "Sort field (default: created_at)"
// @Param sort_dir query string false "Sort direction: asc|desc (default: desc)"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /api/v1/devices [get]
func (h *DeviceHandler) List(c *fiber.Ctx) error {
var filter domain.DeviceFilter
if err := c.QueryParser(&filter); err != nil {
return response.BadRequest(c, "INVALID_QUERY", "Invalid query parameters", err.Error())
}
// Validate filter
if errs := h.validator.Validate(filter); errs != nil {
return response.BadRequest(c, "VALIDATION_ERROR", "Invalid filter parameters", errs)
}
// Get owner from JWT context (set by auth middleware)
ownerID := c.Locals("user_id").(primitive.ObjectID)
devices, total, err := h.deviceUsecase.List(c.Context(), ownerID, filter)
if err != nil {
h.log.Error("Failed to list devices", zap.Error(err))
return response.InternalError(c, "Failed to retrieve devices")
}
// Build pagination meta
page := filter.Page
if page < 1 {
page = 1
}
limit := filter.Limit
if limit < 1 {
limit = 20
}
totalPages := int((total + int64(limit) - 1) / int64(limit))
return response.Paginated(c, devices, &response.Meta{
Page: page,
Limit: limit,
Total: total,
TotalPages: totalPages,
})
}
สังเกตนะครับ
ownerID := c.Locals("user_id")— ตรงนี้คือการดึง user ID จาก JWT token ที่ middleware parse ไว้ให้แล้ว เราไม่ต้อง parse เองอีกรอบ เหมือนพนักงานต้อนรับที่รู้แขกคนไหนมาจากบัตรที่ security เช็คให้แล้ว
Create — Register Device ใหม่
// Create godoc
// @Summary Register a new device
// @Description Registers a new IoT device and returns its credentials
// @Tags devices
// @Accept json
// @Produce json
// @Param body body domain.CreateDeviceRequest true "Device registration data"
// @Success 201 {object} response.Response
// @Security BearerAuth
// @Router /api/v1/devices [post]
func (h *DeviceHandler) Create(c *fiber.Ctx) error {
var req domain.CreateDeviceRequest
if err := c.BodyParser(&req); err != nil {
return response.BadRequest(c, "INVALID_BODY", "Cannot parse request body", err.Error())
}
if errs := h.validator.Validate(req); errs != nil {
return response.BadRequest(c, "VALIDATION_ERROR", "Validation failed", errs)
}
ownerID := c.Locals("user_id").(primitive.ObjectID)
device, err := h.deviceUsecase.Register(c.Context(), ownerID, &req)
if err != nil {
if isConflictError(err) {
return response.BadRequest(c, "DEVICE_ID_TAKEN", err.Error(), nil)
}
h.log.Error("Failed to register device", zap.Error(err))
return response.InternalError(c, "Failed to register device")
}
// Return device WITH token on creation (only time token is visible)
return response.Created(c, deviceWithToken(device))
}
จุดสำคัญตรงนี้คือ deviceWithToken(device) ครับ — token จะแสดงแค่ตอน register ครั้งเดียวเท่านั้น! เหมือนรหัส OTP ที่ใช้ได้แค่ครั้งแรก ถ้าทำหาย device ต้องมา register ใหม่
GetByID, Update, Delete — ทีมCRUD ที่เหลือ
// GetByID godoc
// @Summary Get device by ID
// @Description Returns a single device by its MongoDB ObjectID
// @Tags devices
// @Produce json
// @Param id path string true "Device MongoDB ObjectID"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /api/v1/devices/{id} [get]
func (h *DeviceHandler) GetByID(c *fiber.Ctx) error {
id := c.Params("id")
if _, err := primitive.ObjectIDFromHex(id); err != nil {
return response.BadRequest(c, "INVALID_ID", "Invalid device ID format", nil)
}
ownerID := c.Locals("user_id").(primitive.ObjectID)
device, err := h.deviceUsecase.GetByID(c.Context(), id, ownerID)
if err != nil {
if err.Error() == "access denied" {
return response.NotFound(c, "Device")
}
h.log.Error("Failed to get device", zap.Error(err), zap.String("id", id))
return response.InternalError(c, "Failed to retrieve device")
}
if device == nil {
return response.NotFound(c, "Device")
}
return response.OK(c, device)
}
// Update godoc
// @Summary Update device
// @Description Updates device fields. Only provided fields are updated (PATCH semantics via PUT)
// @Tags devices
// @Accept json
// @Produce json
// @Param id path string true "Device MongoDB ObjectID"
// @Param body body domain.UpdateDeviceRequest true "Fields to update"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /api/v1/devices/{id} [put]
func (h *DeviceHandler) Update(c *fiber.Ctx) error {
id := c.Params("id")
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
return response.BadRequest(c, "INVALID_ID", "Invalid device ID format", nil)
}
var req domain.UpdateDeviceRequest
if err := c.BodyParser(&req); err != nil {
return response.BadRequest(c, "INVALID_BODY", "Cannot parse request body", err.Error())
}
if errs := h.validator.Validate(req); errs != nil {
return response.BadRequest(c, "VALIDATION_ERROR", "Validation failed", errs)
}
ownerID := c.Locals("user_id").(primitive.ObjectID)
device, err := h.deviceUsecase.Update(c.Context(), oid, ownerID, &req)
if err != nil {
h.log.Error("Failed to update device", zap.Error(err))
return response.InternalError(c, "Failed to update device")
}
if device == nil {
return response.NotFound(c, "Device")
}
return response.OKWithMessage(c, "Device updated successfully", device)
}
// Delete godoc
// @Summary Delete device
// @Description Soft-deletes a device. The device data is retained but hidden from queries.
// @Tags devices
// @Produce json
// @Param id path string true "Device MongoDB ObjectID"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /api/v1/devices/{id} [delete]
func (h *DeviceHandler) Delete(c *fiber.Ctx) error {
id := c.Params("id")
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
return response.BadRequest(c, "INVALID_ID", "Invalid device ID format", nil)
}
ownerID := c.Locals("user_id").(primitive.ObjectID)
if err := h.deviceUsecase.Delete(c.Context(), oid, ownerID); err != nil {
if err.Error() == "not found" {
return response.NotFound(c, "Device")
}
h.log.Error("Failed to delete device", zap.Error(err))
return response.InternalError(c, "Failed to delete device")
}
return response.OKWithMessage(c, "Device deleted successfully", nil)
}
Soft Delete คืออะไร? แทนที่จะลบข้อมูลออกจาก DB จริงๆ เราแค่ set flag
deleted_atไว้ — เหมือนพนักงานที่ “resign” แต่ยังเก็บประวัติไว้ในระบบ HR ครับ ถ้าวันนึงต้องการย้อนดูข้อมูลก็ยังหาเจอ
Send Command — สั่งงาน Device จากระยะไกล
// SendCommand godoc
// @Summary Send command to device
// @Description Publishes a command message to the device via MQTT
// @Tags devices
// @Accept json
// @Produce json
// @Param id path string true "Device MongoDB ObjectID"
// @Param body body DeviceCommandRequest true "Command payload"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /api/v1/devices/{id}/commands [post]
func (h *DeviceHandler) SendCommand(c *fiber.Ctx) error {
id := c.Params("id")
if _, err := primitive.ObjectIDFromHex(id); err != nil {
return response.BadRequest(c, "INVALID_ID", "Invalid device ID format", nil)
}
var req DeviceCommandRequest
if err := c.BodyParser(&req); err != nil {
return response.BadRequest(c, "INVALID_BODY", "Cannot parse request body", err.Error())
}
if errs := h.validator.Validate(req); errs != nil {
return response.BadRequest(c, "VALIDATION_ERROR", "Validation failed", errs)
}
ownerID := c.Locals("user_id").(primitive.ObjectID)
if err := h.deviceUsecase.SendCommand(c.Context(), id, ownerID, req.Command, req.Payload); err != nil {
h.log.Error("Failed to send command", zap.Error(err))
return response.InternalError(c, "Failed to send command")
}
return response.OKWithMessage(c, "Command sent successfully", fiber.Map{
"device_id": id,
"command": req.Command,
})
}
// DeviceCommandRequest is the body for sending a command to a device
type DeviceCommandRequest struct {
Command string `json:"command" validate:"required,oneof=reboot reset update_firmware set_interval"`
Payload map[string]interface{} `json:"payload"`
}
// deviceWithToken wraps a device response including the token (registration only)
type deviceWithTokenResponse struct {
*domain.Device
Token string `json:"token"`
}
func deviceWithToken(d *domain.Device) *deviceWithTokenResponse {
return &deviceWithTokenResponse{Device: d, Token: d.Token}
}
func isConflictError(err error) bool {
return err != nil && (contains(err.Error(), "already exists") || contains(err.Error(), "already registered"))
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsStr(s, substr))
}
func containsStr(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}
command ที่ส่งได้มี: reboot, reset, update_firmware, set_interval — ผ่าน MQTT ไปยัง device โดยตรงครับ เหมือนส่ง SMS สั่งงานให้พนักงานภาคสนาม
Bulk Operations — จัดการหมู่คณะ
internal/handler/device_bulk_handler.go
ทำไมต้อง Bulk? ลองนึกว่ามี device 200 ตัวที่ต้องย้าย group พร้อมกัน ถ้าต้องเรียก API ทีละตัว = 200 requests = ช้าและเปลือง bandwidth ครับ Bulk ทำให้จบใน 1 call
+-----------+ +----------+ +----------+
| Client | --> | Bulk API | --> | device_1 |
+-----------+ | | --> | device_2 |
1 request | | --> | ... |
+----------+ | device_N |
+----------+
แทนที่จะส่ง N requests แยกกัน!
package handler
import (
"github.com/gofiber/fiber/v2"
"go.mongodb.org/mongo-driver/bson/primitive"
"github.com/kangana1024/iot-workshop/backend/pkg/response"
)
// BulkOperation defines what to do with selected devices
type BulkOperation string
const (
BulkOpDelete BulkOperation = "delete"
BulkOpUpdateGroup BulkOperation = "update_group"
BulkOpAddTags BulkOperation = "add_tags"
BulkOpRemoveTags BulkOperation = "remove_tags"
)
// BulkRequest is the body for bulk operations
type BulkRequest struct {
Operation BulkOperation `json:"operation" validate:"required,oneof=delete update_group add_tags remove_tags"`
DeviceIDs []string `json:"device_ids" validate:"required,min=1,max=100,dive,mongodb_id"`
Params map[string]interface{} `json:"params"`
}
// BulkResult reports the outcome of each device in the bulk operation
type BulkResult struct {
Succeeded []string `json:"succeeded"`
Failed map[string]string `json:"failed"`
Total int `json:"total"`
}
// Bulk godoc
// @Summary Bulk device operations
// @Description Perform an operation on multiple devices at once
// @Tags devices
// @Accept json
// @Produce json
// @Param body body BulkRequest true "Bulk operation"
// @Success 200 {object} response.Response
// @Security BearerAuth
// @Router /api/v1/devices/bulk [post]
func (h *DeviceHandler) Bulk(c *fiber.Ctx) error {
var req BulkRequest
if err := c.BodyParser(&req); err != nil {
return response.BadRequest(c, "INVALID_BODY", "Cannot parse request body", err.Error())
}
if errs := h.validator.Validate(req); errs != nil {
return response.BadRequest(c, "VALIDATION_ERROR", "Validation failed", errs)
}
ownerID := c.Locals("user_id").(primitive.ObjectID)
result := &BulkResult{
Total: len(req.DeviceIDs),
Succeeded: make([]string, 0),
Failed: make(map[string]string),
}
for _, idStr := range req.DeviceIDs {
oid, err := primitive.ObjectIDFromHex(idStr)
if err != nil {
result.Failed[idStr] = "invalid ID format"
continue
}
var opErr error
switch req.Operation {
case BulkOpDelete:
opErr = h.deviceUsecase.Delete(c.Context(), oid, ownerID)
case BulkOpUpdateGroup:
groupID, _ := req.Params["group_id"].(string)
opErr = h.deviceUsecase.UpdateGroup(c.Context(), oid, ownerID, groupID)
case BulkOpAddTags:
tags := toStringSlice(req.Params["tags"])
opErr = h.deviceUsecase.AddTags(c.Context(), oid, ownerID, tags)
case BulkOpRemoveTags:
tags := toStringSlice(req.Params["tags"])
opErr = h.deviceUsecase.RemoveTags(c.Context(), oid, ownerID, tags)
}
if opErr != nil {
result.Failed[idStr] = opErr.Error()
} else {
result.Succeeded = append(result.Succeeded, idStr)
}
}
return response.OK(c, result)
}
func toStringSlice(v interface{}) []string {
raw, ok := v.([]interface{})
if !ok {
return nil
}
out := make([]string, 0, len(raw))
for _, item := range raw {
if s, ok := item.(string); ok {
out = append(out, s)
}
}
return out
}
สังเกตว่า BulkResult มีทั้ง Succeeded และ Failed แยกกัน — เพราะ partial success เป็นเรื่องปกติในโลก IoT ครับ device บางตัวอาจถูกลบไปแล้ว หรือไม่ใช่เจ้าของก็ได้
Device Status Monitor — เฝ้าระวัง 24/7
internal/usecase/device_status_usecase.go
ทำไมต้องมี Status Monitor? เพราะ IoT device มันอาจ “หาย” ได้โดยไม่แจ้งลา — ไฟดับ, network ขาด, หรือ firmware crash ระบบ monitor นี้จะ check ทุก 30 วินาที ว่า device ไหน “เงียบ” ผิดปกติ แล้ว mark ว่า offline
เหมือนพี่ supervisor ที่เดินตรวจพนักงาน ถ้าใครหายไปจากโต๊ะนานเกินไป ก็จดว่า “absent” ให้เลย (ノ◕ヮ◕)ノ*:・゚✧
package usecase
import (
"context"
"time"
"go.uber.org/zap"
"github.com/kangana1024/iot-workshop/backend/internal/domain"
"github.com/kangana1024/iot-workshop/backend/internal/repository"
)
// DeviceStatusMonitor runs a background goroutine to mark stale devices as offline
type DeviceStatusMonitor struct {
deviceRepo repository.DeviceRepository
log *zap.Logger
interval time.Duration
stopCh chan struct{}
}
// NewDeviceStatusMonitor creates a new monitor
func NewDeviceStatusMonitor(repo repository.DeviceRepository, log *zap.Logger) *DeviceStatusMonitor {
return &DeviceStatusMonitor{
deviceRepo: repo,
log: log,
interval: 30 * time.Second,
stopCh: make(chan struct{}),
}
}
// Start begins the background monitoring loop
func (m *DeviceStatusMonitor) Start(ctx context.Context) {
m.log.Info("Device status monitor started", zap.Duration("interval", m.interval))
ticker := time.NewTicker(m.interval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
m.checkStaleDevices(ctx)
case <-m.stopCh:
m.log.Info("Device status monitor stopped")
return
case <-ctx.Done():
return
}
}
}
// Stop signals the monitor to stop
func (m *DeviceStatusMonitor) Stop() {
close(m.stopCh)
}
func (m *DeviceStatusMonitor) checkStaleDevices(ctx context.Context) {
filter := domain.DeviceFilter{Page: 1, Limit: 100}
staleDevices, err := m.deviceRepo.FindStaleDevices(ctx, filter)
if err != nil {
m.log.Error("Failed to find stale devices", zap.Error(err))
return
}
for _, device := range staleDevices {
if err := m.deviceRepo.UpdateStatus(ctx, device.DeviceID, domain.DeviceStatusOffline); err != nil {
m.log.Error("Failed to mark device offline",
zap.String("device_id", device.DeviceID),
zap.Error(err),
)
} else {
m.log.Debug("Device marked offline", zap.String("device_id", device.DeviceID))
}
}
if len(staleDevices) > 0 {
m.log.Info("Stale device check complete",
zap.Int("marked_offline", len(staleDevices)),
)
}
}
selectใน Go นี่เจ๋งมากครับ มันรอหลาย channel พร้อมกัน แล้ว “แย่งกัน” respond — เหมือนพนักงาน call center ที่รับโทรศัพท์หลายสายในเวลาเดียวกัน ใครโทรมาก่อนก็รับก่อน แต่ถ้ามีสายพิเศษ “หยุดการทำงาน” ก็วางสายทั้งหมดแล้วออก
Swagger Documentation Setup
ทำไม Swagger ถึงสำคัญ?
ลองคิดดูว่า front-end developer จะรู้ได้ยังไงว่า API รับ parameter อะไร, return อะไร? ถ้าต้องมานั่งถามทีม back-end ทุกครั้ง — เสียเวลามาก Swagger สร้าง “manual อัตโนมัติ” จาก annotation ใน code เลยครับ
ติดตั้ง swaggo
go install github.com/swaggo/swag/cmd/swag@latest
go get github.com/swaggo/fiber-swagger
go get github.com/swaggo/swag
cmd/server/main.go — เพิ่ม Swagger annotation
// @title IoT Workshop API
// @version 1.0
// @description IoT Platform Backend API for device management, sensor data, and real-time monitoring
// @termsOfService http://swagger.io/terms/
// @contact.name IoT Workshop Team
// @contact.url https://github.com/kangana1024/iot-workshop
// @license.name MIT
// @host localhost:8080
// @BasePath /api/v1
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description Type "Bearer" followed by a space and the JWT token.
package main
สร้าง Swagger docs
swag init -g cmd/server/main.go -o docs/
# เพิ่ม route ใน router.go
import fiberSwagger "github.com/swaggo/fiber-swagger"
import _ "github.com/kangana1024/iot-workshop/backend/docs"
app.Get("/swagger/*", fiberSwagger.WrapHandler)
หลังจาก run swag init แล้ว เปิด browser ไปที่ http://localhost:8080/swagger/index.html จะเห็น interactive API docs ทันที ลอง call API จากหน้าเว็บได้เลยโดยไม่ต้องใช้ curl!
ทดสอบ API กัน!
Register Device
curl -X POST http://localhost:8080/api/v1/devices \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"device_id": "sensor-temp-001",
"name": "Temperature Sensor - Building A",
"description": "DHT22 sensor on floor 3",
"type": "sensor",
"location": {
"latitude": 13.7563,
"longitude": 100.5018,
"label": "Bangkok Office"
},
"tags": ["temperature", "humidity", "building-a"],
"metadata": {
"model": "DHT22",
"firmware": "1.2.0"
}
}'
List Devices with Filters
# Get online sensors, newest first
curl "http://localhost:8080/api/v1/devices?status=online&type=sensor&sort_by=last_seen_at&sort_dir=desc&page=1&limit=20" \
-H "Authorization: Bearer $TOKEN"
# Search devices
curl "http://localhost:8080/api/v1/devices?search=temperature&page=1&limit=10" \
-H "Authorization: Bearer $TOKEN"
Bulk Delete
curl -X POST http://localhost:8080/api/v1/devices/bulk \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"operation": "delete",
"device_ids": [
"65f1234567890abcdef12345",
"65f1234567890abcdef12346"
]
}'
Send Command
curl -X POST http://localhost:8080/api/v1/devices/65f1234567890abcdef12345/commands \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"command": "set_interval",
"payload": {
"interval_seconds": 30
}
}'
Response Examples — หน้าตา Response ที่ได้กลับมา
Successful List Response
{
"success": true,
"data": [
{
"id": "65f1234567890abcdef12345",
"device_id": "sensor-temp-001",
"name": "Temperature Sensor - Building A",
"type": "sensor",
"status": "online",
"last_seen_at": "2026-03-26T10:30:00Z",
"created_at": "2026-03-01T08:00:00Z"
}
],
"meta": {
"page": 1,
"limit": 20,
"total": 42,
"total_pages": 3
}
}
Validation Error Response
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": {
"device_id": "device_id is required",
"type": "type must be one of: sensor actuator gateway camera"
}
}
}
Error response ที่ดีต้องบอกชัดว่า field ไหนผิด ผิดยังไง — เหมือนครูที่แก้การบ้านแล้วบอกว่าข้อไหนผิดและเพราะอะไร ไม่ใช่แค่เขียน “ผิด” ไว้ครับ
สรุปสิ่งที่เราได้ทำวันนี้
(ง •̀_•́)ง ทำได้ครบทุกอย่าง!
- สร้าง CRUD REST API ครบชุดสำหรับ Device Management
- ใช้ Pagination, Filtering, Sorting แบบ flexible ด้วย QueryParser
- สร้าง Device Registration Flow ที่ออก Auth Token ครั้งเดียวตอน register
- ติดตั้ง Device Status Monitor ที่ทำงาน background ตรวจ stale devices ทุก 30 วินาที
- สร้าง Bulk Operations สำหรับจัดการหลาย device พร้อมกัน
- ตั้งค่า Swagger Documentation อัตโนมัติจาก annotation ใน code
ขั้นตอนต่อไป
- ก่อนหน้า: IoT Workshop #5: MongoDB Models & Repository
- ถัดไป: IoT Workshop #7: Sensor Data Ingestion
ตอนหน้าเราจะมาทำ Sensor Data Ingestion — รับข้อมูล sensor ที่ไหลเข้ามาตลอดเวลาแบบ real-time กัน มาลุยต่อกันนะครับน้องๆ! ٩(◕‿◕。)۶