Articles

Ash Framework vs writing a traditional backend

I've written a lot of backends — both the traditional way (separate controller + service + repository) and full-framework style. Lately I've been going deep on Ash Framework for Elixir, so I wanted to write an honest head-to-head comparison. No selling, no exaggeration — just the real pros and cons.

Ash isn't just an ORM or web framework — it's adeclarative backend framework. You define "resources" (like Post, User, Order) with actions, attributes, and policies, then extensions auto-generate GraphQL, REST APIs, authentication, authorization, and background jobs for you.

The same task, two ways

Say we're building a simple blog system — posts, authors, auth, a REST API, and a background job for publishing.

  • Traditional way — write model, migration, controller, serializer, routes, auth middleware, policy checks, job worker separately. Each piece is 50–200 lines, easily thousands of lines for a single feature
  • Ash way — declare one resource covering actions, attributes, policies, API endpoints, auth strategy, and oban triggers. The actual resource code is usually 50–150 lines

The numbers don't lie — for CRUD + API + auth + jobs that repeat,Ash saves real code at 5–10x because you're not writing the same plumbing over and over.

Real code example: a Blog Post

Let's look at actual code — say we need a Post resource with title, content, status (draft/published), and an author. It needs auth, owner-only permissions, and a REST API.

The Ash way — declare once, get everything

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
    # Everyone can only read published posts
    policy action_type(:read) do
      authorize_if expr(status == :published)
    end

    # Only the owner can update/destroy
    policy action_type([:update, :destroy]) do
      authorize_if expr(author_id == ^actor(:id))
    end
  end
end

This single file gives you: migrations, validation, policy enforcement, and typed actions callable from other code. Want GraphQL? Just add extensions: [AshGraphql.Resource] and agraphql block — you get a full API without writing resolvers.

# Calling it — actions are plain typed functions
MyApp.Blog.Post.publish!(%{
  title: "Hello Ash",
  content: "...",
  author_id: user.id
}, actor: user)

# And you get these GraphQL queries/mutations for free:
#   post(id:), posts, createPost, updatePost, deletePost

The traditional way — write every piece yourself

// routes/posts.ts — write each endpoint separately
import { Router } from "express";
import { prisma } from "../db";
import { requireAuth } from "../middleware/auth";

const router = Router();

// GET /posts — write the query + filter + serialization yourself
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 yourself
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 — check ownership every single time
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);
});

That's just 3 basic endpoints — not counting migrations, serializers, centralized error handling, tests, and the GraphQL schema you'd still need to write yourself. Every new endpoint repeats the same pattern, while in Ash you just declare a new action on the same resource.

Pros you'll actually feel

  • Everything is introspectable — actions, attributes, policies are readable at runtime, so extensions auto-generate GraphQL/JSON:API/policies without you writing serializers twice
  • Built-in authorization — write a policy once, it applies to every action and filters data automatically. Enumeration attack protection is built in — no remembering where to check permissions
  • Elixir underneath — you get BEAM VM concurrency, fault tolerance, and pattern matching for free, no thread management needed
  • Domain-first — the code reads like your business spec, newcomers understand it faster than controller spaghetti
  • Complete ecosystem — AshAuthentication, AshOban, AshGraphql, AshJsonApi, AshCloak (encryption), AshAi (MCP tools) — plug in piece by piece as needed

Cons you have to accept

  • Steep learning curve — you need to understand Elixir, OTP, and Ash's own paradigm. Early on you'll write slower than usual. If nobody on the team has used Elixir, expect 2–4 weeks to get comfortable
  • Harder debugging — most code is macro/declarative, so errors sometimes have long, hard-to-read stack traces. You'll rely on docs and community more
  • Smaller community than mainstream — weird problems may not have StackOverflow answers; you'll need the forum or Discord
  • Vendor-ish ecosystem — many extensions are by the Ash core team; roadmap changes could affect your project
  • Overkill for small projects — if it's just a landing page + contact form, don't use Ash. Next.js or Astro will finish faster

Cost-effectiveness: when it's worth it, when it's not

Worth it when your project has complex domain models — e-commerce, CRM, SaaS, multi-entity management systems. Ash excels at declaring once and getting many things. The more features, the clearer the difference.

Not worth it when the team has zero Elixir background and a tight deadline — the time spent learning the framework may exceed the benefit in the short term, or if the project is a prototype you'll throw away in two weeks.

Worth it long-term when the project has a long lifecycle — maintenance, feature additions, refactoring. Declarative code means changing a policy or adding a field automatically affects everything related, no need to hunt down every file.

Code efficiency: how to measure it

It's not just "writing less" — it's writing wrong less often. Because actions are typed, policies enforce at the data layer, and extensions generate consistent APIs, bug types like "forgot to check permission" or "serializer missing a field" barely exist — there's nowhere to forget.

Compared to traditional boilerplate-heavy code where every line is a chance to slip up, Ash moves that risk into a framework that's been tested by the community.

Bottom line

If you're building a serious backend with real business logic — many entities, many roles, many API endpoints — and your team can learn Elixir, Ash is worth it. You'll get a system that's easier to maintain, writes less code, and scales without big refactors.

But if your team is all PHP/Node/Python, or the project is an MVP that needs to ship fast — writing traditional code in a language the team already knows is still the more reasonable choice.

No framework is "the best" for everyone, but Ash is genuinely interesting if you're tired of boilerplate and want code that tells the business story rather than the infrastructure. Check it out at ash-hq.org