# Primeira jornada > Uma pessoa abre uma página, executa uma ação e vê o resultado ou um erro útil — com endpoint privado, permissão e tratamento de erro. Comece sempre por uma jornada pequena e completa: **página + navegação + endpoint + permissão**. Este exemplo é uma ação sem persistência; para cadastro, veja [Models e CRUD](/docs/backend/models-e-crud). ## 1. Declare a permissão Em `central.app.json`, adicione `demo.execute` a `rbac.permissions`. Mantenha o admin existente (que já tem `"*"`). ```json title="central.app.json" { "rbac": { "permissions": ["demo.execute"], "roles": [ { "code": "admin", "name": "Administrador", "admin": true, "permissions": ["*"] }, { "code": "operator", "name": "Operador", "permissions": ["demo.execute"] }, { "code": "viewer", "name": "Consulta", "permissions": [] } ] } } ``` ## 2. Crie a rota privada ```javascript title="backend/src/routes/demo.js" const { defineRoutes, GenericError } = require("@oondemand/oon-core-back"); defineRoutes("/demo", (router) => { router.private.post("/saudacao", { permission: "demo.execute" }, async (req, res) => { const nome = typeof req.body?.nome === "string" ? req.body.nome.trim() : ""; if (!nome || nome.length > 80) { throw new GenericError("Informe um nome de até 80 caracteres.", { statusCode: 422, code: "DEMO_NAME_INVALID", }); } res.json({ message: `Olá, ${nome}!` }); }); }); ``` A assinatura é `router.private[método](caminho, opções, handler)`. O arquivo em `src/routes` é autocarregado — não é preciso exportar nada. ## 3. Crie a página A rota `/` e o item **Início** já existem no scaffold. Substitua a página: ```tsx title="frontend/src/pages/HomePage.tsx" import { useState, type FormEvent } from "react"; import { useOonApi } from "@oondemand/oon-core-front"; export function HomePage() { const { http } = useOonApi(); const [nome, setNome] = useState(""); const [resultado, setResultado] = useState(""); const [busy, setBusy] = useState(false); async function submit(event: FormEvent) { event.preventDefault(); setBusy(true); try { const response = await http.post("/demo/saudacao", { nome }); setResultado(response.data.message); } catch (error: unknown) { const response = (error as { response?: { status?: number; data?: { message?: string; error?: { code?: string; message?: string }; } } }).response; setResultado(response?.data?.error?.message || response?.data?.message || `Não foi possível executar a ação (HTTP ${response?.status ?? "indisponível"}).`); } finally { setBusy(false); } } return (

Primeira jornada

{resultado}

); } ``` > [!TIP] > O cliente do Core cuida de sessão e CSRF. Não crie token fixo nem chame `fetch` direto para o backend do App. ## 4. Prove a jornada Reinicie `npm run dev` após incluir o módulo e verifique: | Cenário | Resultado esperado | |---|---| | Perfil `admin`, nome válido | `200` com `Olá, Fulano!` | | Perfil `admin`, nome vazio | `422` com código `DEMO_NAME_INVALID` | | Perfil `viewer` (sem `demo.execute`) | `403` — recusa no backend | Troque o perfil pelo banner do runtime local. A permissão é recalculada no backend a cada requisição. ## Erros que você vai encontrar - **`OON_ROUTE_REGISTRATION_INVALID`** — você passou um middleware (como `requirePermission(...)`) na posição das opções. Use `{ permission }` no segundo argumento. - **404 em HTML** — o módulo não está em `backend/src/routes`, o processo não foi reiniciado ou você duplicou `/api` na rota. Não é um erro de domínio. - **403 CSRF local** — o frontend não está passando pelo proxy `/api` do Vite. ## Próximo passo ```cards [ { "title": "Rotas privadas", "href": "/docs/backend/rotas-privadas", "description": "Opções, permissões, auditoria e sensitiveBody." }, { "title": "Models e CRUD", "href": "/docs/backend/models-e-crud", "description": "Transforme a jornada em cadastro persistido." } ] ``` Fonte: https://sdk.oonapps.com/docs/introducao/primeira-jornada