# Example: blog API [#example-blog-api] A small blog from schema to frontend calls. Everything here was run against released packages; the outputs shown are real. The frontend calls import the client as `./api/AvClient`, the Vite and Next.js spelling; a `nodenext` ESM project writes `./api/AvClient.js` ([Importing the client](/framework/docs/client-setup#importing-the-client)). ## 1. Create the server [#1-create-the-server] ```bash npx @aventara/cli@rc new blog-api --yes --skip-git cd blog-api ``` ## 2. The schema [#2-the-schema] Replace the starter `User` model in `prisma/schema.prisma` (keep the `generator` and `datasource` blocks as generated): ```prisma enum Role { READER AUTHOR ADMIN } model User { id Int @id @default(autoincrement()) email String @unique name String? passwordHash String @default("") internalNotes String? role Role @default(READER) createdAt DateTime @default(now()) posts Post[] } model Category { id Int @id @default(autoincrement()) name String @unique posts Post[] } model Post { id Int @id @default(autoincrement()) title String body String? published Boolean @default(false) views Int @default(0) createdAt DateTime @default(now()) author User @relation(fields: [authorId], references: [id]) authorId Int category Category? @relation(fields: [categoryId], references: [id]) categoryId Int? } ``` Create the tables and regenerate what Aventara reads from the schema: ```bash npx prisma db push npm run aventara:prepare ``` ## 3. The configuration [#3-the-configuration] `src/aventara.config.ts` (the scaffold's `src/aventara.adapter.ts` already holds the adapter, so the configuration has none): ```ts import { AvError, type AvGuard } from "@aventara/core"; // Remote callers must send an Authorization header to write. const requireSignIn: AvGuard = (ctx) => { if (ctx.$operation.family !== "find" && !ctx.$transport?.headers["authorization"]) { throw new AvError("A4000", "Sign in first."); } return true; }; export const aventaraConfig = { api: "/api", exposed: true, client: { exposed: true, exemptions: { resources: { User: { fields: { // write-only: can be set on create/update, never read, filtered or ordered passwordHash: ["select", "filter", "order"], // not in the client layer at all internalNotes: true, }, operations: { delete: ["first", "count"] }, }, Post: { operations: { delete: ["count"] } }, }, }, limits: { maxListLimit: 50 }, pipelines: { guards: [requireSignIn] }, }, } as const; ``` What each part does, all for remote callers (the `client` layer), leaving server-side code untouched: | Setting | Effect | | --------------------------------------------- | --------------------------------------------------------------------------------- | | `passwordHash: ["select", "filter", "order"]` | Settable on create and update, but selecting, filtering or ordering by it fails. | | `internalNotes: true` | The field does not exist for remote callers. | | `User` `delete` switched off except `unique` | No `first` or `count` deletes for users, so no filtered mass deletion. | | `maxListLimit: 50` | A list may ask for at most 50 rows. | | `requireSignIn` guard | Anything that is not a `find` needs an `Authorization` header. Reads stay public. | See [Configuration](/framework/docs/configuration) for the full model, [Exposure](/framework/docs/exposure) for `exemptions`, and [Contract layers](/framework/docs/contract-layers) for how layers relate. ## 4. Run the server [#4-run-the-server] ```bash PORT=3000 npm run start:dev ``` ```text Aventara mounted 42 operations at /api (+ GET /api/_contract, POST /api/_transactions, POST /api/_all) ``` A browser frontend on another origin needs CORS. The scaffold's `src/main.ts` already enables it for the origins in `CORS_ORIGINS` (`http://localhost:5173,http://localhost:3001` in `.env`), so a Vite frontend works as it is; add your own origin to that list when you deploy ([Deploying](/framework/docs/deploying#cors-and-browsers)). ## 5. Generate the client [#5-generate-the-client] In the frontend project: ```bash npx @aventara/client@rc init --yes --entrypoint http://localhost:3000/api ``` ## 6. Frontend calls [#6-frontend-calls] ```ts import avClient, { AvClient, AvConflictError, AvAuthError } from "./api/AvClient"; // Signed-in client: the server's guard wants an Authorization header on writes. const api = new AvClient({ fetch: (input, init) => fetch(input, { ...init, headers: { ...init?.headers, Authorization: `Bearer ${getToken()}` } }), }); ``` ### Read the feed [#read-the-feed] ```ts const feed = await avClient.Post.find.many({ where: { published: true }, select: ["id", "title", "views", { author: { select: ["name"] } }, { category: { select: ["name"] } }], orderBy: [{ createdAt: "desc" }], limit: 10, }); ``` ```json [ { "id": 3, "title": "Related", "views": 0, "author": { "name": "Grace" }, "category": { "name": "Tech" } }, { "id": 1, "title": "Hello", "views": 0, "author": { "name": "Ada" }, "category": { "name": "Tech" } } ] ``` ### Posts per category [#posts-per-category] ```ts const categories = await avClient.Category.find.many({ select: ["id", "name", { posts: { select: ["$count"] } }], orderBy: { name: "asc" }, }); // [{ id: 1, name: "Tech", posts: { count: 2 } }, ...] ``` ### Sign up an author with a first post: a transaction [#sign-up-an-author-with-a-first-post-a-transaction] The category and the post need the new author's id, and either all three rows exist afterwards or none do: ```ts const author = api.tx.User.create.one({ data: { email: "lin@example.com", name: "Lin", role: "AUTHOR" }, select: ["id"], }); const category = api.tx.Category.create.one({ data: { name: "Notes" }, select: ["id"] }); const first = api.tx.Post.create.one({ data: { title: "Hello, blog", body: "First!", published: true, authorId: author.$ref("id"), categoryId: category.$ref("id"), }, }); const [a, c, p] = await api.transaction([author, category, first]); // a: { id: 4 } c: { id: 5 } p.title: "Hello, blog" ``` Roll back on a conflict (the same email twice): ```ts try { const extra = api.tx.Category.create.one({ data: { name: "should-not-exist" } }); const again = api.tx.User.create.one({ data: { email: "lin@example.com" }, select: ["id"] }); await api.transaction([extra, again]); } catch (e) { if (e instanceof AvConflictError) { console.log(e.code, e.cause.operation); // "A2008" 1 (step 1 failed) } } await avClient.Category.find.first({ where: { name: "should-not-exist" } }); // null ``` See [Transactions](/framework/docs/transactions). ### Count a view [#count-a-view] ```ts await api.Post.update.unique({ where: { id: 7 }, data: { views: { $increment: 1 } }, select: ["id", "views"], }); // { id: 7, views: 1 } ``` ### What the configuration enforces [#what-the-configuration-enforces] ```ts // no Authorization header try { await avClient.Post.create.one({ data: { title: "anon", authorId: 1 } }); } catch (e) { // e instanceof AvAuthError, e.code === "A4000", e.cause.message === "Sign in first." } // removed operation: it does not exist on the client, in the type or at runtime typeof avClient.User.delete.first; // type error; at runtime "undefined" // list limit await avClient.Post.find.many({ limit: 100 }); // AvValidationError A2009: "limit exceeds maxListLimit (50)." // write-only field await avClient.User.find.many({ where: { passwordHash: "x" } }); // type error; at runtime AvValidationError: 'Filtering is not available on field "passwordHash".' // hidden field await avClient.User.find.many({ select: ["internalNotes"] }); // type error; at runtime AvValidationError: 'Field "internalNotes" is not available on Resource "User".' ``` (The last three are rejected by the TypeScript types before they run; the comments show what the server answers if a caller ignores them.) ## Next [#next] * [Example: admin vs public](/framework/docs/example-admin-vs-public): the same API used by trusted server code. * [Example: recipes](/framework/docs/example-recipes) * [Client errors](/framework/docs/client-errors) ## See also [#see-also] * [Configuration](/framework/docs/configuration) * [Typed calls](/framework/docs/typed-calls) * [Client setup](/framework/docs/client-setup) * [Transactions](/framework/docs/transactions) * Guides: [Exposing and hiding fields](/framework/docs/exposing-and-hiding-fields), [Authentication and guards](/framework/docs/authentication-and-guards), [Relations and nested writes](/framework/docs/relations-and-nested-writes), [Transactions with $ref](/framework/docs/transactions-with-ref)