ใช้ Ash Framework vs เขียน backend แบบดั้งเดิม
ผมเขียน backend มาเยอะพอสมควร ทั้งแบบดั้งเดิม (controller + service + repository แยกกันเอง) และแบบใช้ framework เต็มตัว ช่วงนี้ได้ลงลึก Ash Framework ของ Elixir มากขึ้น เลยอยากเขียนเปรียบเทียบแบบตรงๆ ไม่ขายของ ไม่เกินจริง — มีทั้งข้อดีและข้อเสียที่ต้องยอมรับ
Ash ไม่ใช่แค่ ORM หรือ web framework แต่มันคือ declarative backend frameworkคุณนิยาม "resource" (เช่น Post, User, Order) พร้อม actions, attributes, policies แล้ว extension ต่างๆ จะสร้าง GraphQL, REST API, authentication, authorization, background jobs ต่อให้เองอัตโนมัติ
งานเดียวกัน สองวิธี
สมมติทำระบบ blog ง่ายๆ — มี post, มี author, ต้องมี auth, ต้องมี REST API, ต้องมี background job สำหรับ publish
- แบบดั้งเดิม — เขียน model, migration, controller, serializer, route, auth middleware, policy check, job worker แยกกัน แต่ละชิ้นอาจ 50–200 บรรทัด รวมกันหลายพันบรรทัดสำหรับฟีเจอร์เดียว
- แบบ Ash — ประกาศ resource เดียว ครอบ actions, attributes, policies, API endpoints, auth strategy, oban triggers แล้วปล่อยให้ extension ทำงาน โค้ดจริงของ resource มักอยู่ที่ 50–150 บรรทัด
ตัวเลขไม่ได้โกหก — สำหรับ CRUD + API + auth + jobs ที่ซ้ำซากAsh ประหยัดโค้ดได้จริง 5–10 เท่าเพราะคุณไม่ได้เขียน plumbing ซ้ำๆ
ตัวอย่างโค้ดจริง: Blog Post
ดูของจริงกัน — สมมติทำ resource Post ที่มี title, content, status (draft/published) และ author ต้องมี auth + เช็คสิทธิ์เจ้าของ + มี REST API
แบบ Ash — declare ครั้งเดียว ได้ทุกอย่าง
defmodule MyApp.Blog.Post do
use Ash.Resource,
domain: MyApp.Blog,
data_layer: AshPostgres.DataLayer,
authorizers: [Ash.Policy.Authorizer]
postgres do
table "posts"
repo MyApp.Repo
end
attributes do
uuid_primary_key :id
attribute :title, :string, allow_nil?: false
attribute :content, :string
attribute :status, :atom,
constraints: [one_of: [:draft, :published]]
create_timestamp :inserted_at
update_timestamp :updated_at
end
relationships do
belongs_to :author, MyApp.Accounts.User
end
actions do
defaults [:read, :destroy]
create :publish do
accept [:title, :content]
argument :author_id, :uuid, allow_nil?: false
change set_attribute(:status, :published)
end
read :published do
filter expr(status == :published)
end
end
policies do
# ทุกคนอ่านได้เฉพาะโพสต์ที่ published
policy action_type(:read) do
authorize_if expr(status == :published)
end
# แก้/ลบได้เฉพาะเจ้าของโพสต์
policy action_type([:update, :destroy]) do
authorize_if expr(author_id == ^actor(:id))
end
end
endแค่ไฟล์เดียวเท่านี้คุณได้: migration, validation, policy enforcement, typed actions ที่เรียกจากโค้ดอื่นได้ และถ้าอยากได้ GraphQL แค่เพิ่ม extensions: [AshGraphql.Resource] + block graphqlก็ได้ API เต็มรูปแบบโดยไม่ต้องเขียน resolver เอง
# เรียกใช้ — action เป็น typed function ธรรมดา
MyApp.Blog.Post.publish!(%{
title: "Hello Ash",
content: "...",
author_id: user.id
}, actor: user)
# ได้ GraphQL queries/mutations เหล่านี้ฟรี:
# post(id:), posts, createPost, updatePost, deletePostแบบดั้งเดิม — ต้องเขียนเองทีละชิ้น
// routes/posts.ts — เขียนแยกกันทุก endpoint
import { Router } from "express";
import { prisma } from "../db";
import { requireAuth } from "../middleware/auth";
const router = Router();
// GET /posts — query + filter + serialize เอง
router.get("/posts", async (req, res) => {
const posts = await prisma.post.findMany({
where: { status: "published" },
});
res.json(posts);
});
// POST /posts/publish — validate + auth + set status เอง
router.post("/posts/publish", requireAuth, async (req, res) => {
const { title, content } = req.body;
if (!title || !content) {
return res.status(400).json({ error: "missing fields" });
}
const post = await prisma.post.create({
data: {
title,
content,
status: "published",
authorId: req.user.id,
},
});
res.status(201).json(post);
});
// PATCH /posts/:id — เช็คสิทธิ์เจ้าของเองทุกครั้ง
router.patch("/posts/:id", requireAuth, async (req, res) => {
const post = await prisma.post.findUnique({
where: { id: req.params.id },
});
if (!post) return res.status(404).end();
if (post.authorId !== req.user.id) {
return res.status(403).end();
}
const updated = await prisma.post.update({
where: { id: req.params.id },
data: req.body,
});
res.json(updated);
});นี่แค่ 3 endpoints พื้นฐาน — ยังไม่ได้รวม migration, serializer, error handling กลาง, tests และ GraphQL schema ที่ต้องเขียนเพิ่มเอง ทุกครั้งที่เพิ่ม endpoint ใหม่ก็ต้องทำซ้ำ pattern เดิม ส่วนแบบ Ash เพิ่ม endpoint ใหม่แค่ประกาศ action เพิ่มใน resource เดิม
ข้อดีที่รู้สึกได้จริง
- Introspectable ทุกอย่าง — actions, attributes, policies อ่านได้ตอน runtime extension เลยสร้าง GraphQL/JSON:API/policies ให้เอง ไม่ต้องเขียน serializer ซ้ำสองสามรอบ
- Authorization ในตัว — policy เขียนครั้งเดียว apply กับทุก action และ filter ข้อมูลให้อัตโนมัติ กัน enumeration attack ได้ในตัว ไม่ต้องจำว่าเช็ค permission ตรงไหนบ้าง
- Elixir underneath — ได้ concurrency, fault tolerance, pattern matching ของ BEAM VM ฟรี ไม่ต้องจัดการ thread เอง
- Domain-first — โค้ดอ่านเหมือน spec ของธุรกิจ คนใหม่เข้ามาเข้าใจเร็วกว่า controller spaghetti
- Ecosystem ครบ — AshAuthentication, AshOban, AshGraphql, AshJsonApi, AshCloak (encryption), AshAi (MCP tools) ต่อได้ทีละชิ้นตามต้องการ
ข้อเสียที่ต้องยอมรับ
- Learning curve สูง — ต้องเข้าใจ Elixir, OTP, และ paradigm ของ Ash เอง ช่วงแรกเขียนช้ากว่าเดิมมาก ถ้าทีมไม่มีใครเคยใช้ Elixir เลย อาจใช้เวลา 2–4 สัปดาห์กว่าจะคล่อง
- Debug ยากกว่า — โค้ดส่วนใหญ่เป็น macro/declarative เวลา error บางที stack trace ยาวและอ่านยาก ต้องพึ่ง docs กับ community
- Community เล็กกว่า mainstream — เจอปัญหาแปลกๆ อาจไม่มีคำตอบใน StackOverflow ทันที ต้องถามใน forum หรือ Discord
- Vendor-ish ecosystem — extension หลายตัวเป็นของ Ash core team เอง ถ้า roadmap เปลี่ยนอาจกระทบโปรเจกต์
- Overkill สำหรับโปรเจกต์เล็ก — ถ้าแค่ landing page + contact form อย่าใช้ Ash เลย ใช้ Next.js หรือ Astro จบเร็วกว่า
ความคุ้มค่า: คุ้มตอนไหน ไม่คุ้มตอนไหน
คุ้ม เมื่อโปรเจกต์มี domain model ซับซ้อน — e-commerce, CRM, SaaS, ระบบจัดการหลาย entity เพราะ Ash เก่งเรื่อง declare ครั้งเดียวได้หลายอย่าง ยิ่งฟีเจอร์เยอะยิ่งเห็นผลต่างชัด
ไม่คุ้ม เมื่อทีมไม่มี Elixir background เลย และ deadline สั้นมาก — เวลาที่เสียไปเรียนรู้ framework อาจมากกว่าประโยชน์ที่ได้ในระยะสั้น หรือถ้าโปรเจกต์เป็น prototype ที่จะทิ้งใน 2 สัปดาห์
คุ้มในระยะยาว เมื่อโปรเจกต์มี lifecycle ยาว — maintenance, เพิ่มฟีเจอร์, refactor เพราะโค้ด declarative ทำให้เปลี่ยน policy หรือเพิ่ม field กระทบทุกจุดที่เกี่ยวข้องอัตโนมัติ ไม่ต้องไล่แก้ทีละไฟล์
ประสิทธิภาพโค้ด: วัดยังไง
ไม่ใช่แค่ "เขียนน้อยกว่า" แต่คือ เขียนผิดน้อยกว่า — เพราะ actions เป็น typed, policies enforce ที่ data layer, extension สร้าง API ที่ consistent กันหมด bug ประเภท "ลืมเช็ค permission" หรือ "serializer ลืม field" แทบไม่มี เพราะไม่มีที่ให้ลืม
เทียบกับแบบดั้งเดิมที่โค้ด boilerplate เยอะ แต่ละบรรทัดคือโอกาสที่จะพลาด — Ash ย้ายความเสี่ยงนั้น ไปไว้ที่ framework ที่ test มาจาก community แล้ว
สรุปตรงๆ
ถ้าคุณเขียน backend ที่มี business logic จริงจัง มีหลาย entity, หลาย role, หลาย API endpoint แล้วทีมพอจะเรียนรู้ Elixir ได้ — Ash คุ้มค่าคุณจะได้ระบบที่ maintain ง่ายกว่า เขียนน้อยกว่า และ scale ได้โดยไม่ต้อง refactor ใหญ่
แต่ถ้าทีมเป็น PHP/Node/Python ล้วน หรือโปรเจกต์เป็น MVP ที่ต้องเสร็จเร็ว — เขียนแบบดั้งเดิมในภาษาที่ทีมคล่องกว่า ยังเป็นทางเลือกที่สมเหตุสมผลกว่า
ไม่มี framework ไหน "ดีที่สุด" สำหรับทุกคน แต่ Ash เป็นตัวเลือกที่น่าสนใจจริงๆ ถ้าคุณเบื่อกับ boilerplate และอยากให้โค้ดบอกเล่า business logic มากกว่า infrastructure — ลองดูได้ที่ ash-hq.org