{"id":371,"date":"2026-10-31T09:29:13","date_gmt":"2026-10-31T13:29:13","guid":{"rendered":"https:\/\/juredev.com\/blog\/?p=371"},"modified":"2026-10-31T09:29:15","modified_gmt":"2026-10-31T13:29:15","slug":"autorizacion-real-en-leptos-protege-tus-server-functions","status":"publish","type":"post","link":"https:\/\/juredev.com\/blog\/2026\/10\/autorizacion-real-en-leptos-protege-tus-server-functions\/","title":{"rendered":"Autorizaci\u00f3n real en Leptos: protege tus server functions, no solo tus rutas"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Redirigir al login en la interfaz no protege nada: cada <code>#[server]<\/code> de Leptos es un endpoint HTTP p\u00fablico, y la autorizaci\u00f3n real tiene que vivir en el servidor. En este art\u00edculo veremos c\u00f3mo blindar una aplicaci\u00f3n full-stack con Leptos y Axum, con c\u00f3digo que puedes revisar en la rama <a href=\"https:\/\/github.com\/jure-ve\/user-leptos-app\/tree\/feat\/autorizacion-server-functions\" target=\"_blank\" rel=\"noopener\"><code>feat\/autorizacion-server-functions<\/code><\/a> de <code>user-leptos-app<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">La ilusi\u00f3n de seguridad con server functions<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">En el <a href=\"https:\/\/juredev.com\/blog\/2026\/02\/full-stack-en-rust-estructurando-aplicaciones-con-leptos\/\" target=\"_blank\" rel=\"noopener\">art\u00edculo anterior<\/a> estructuramos una aplicaci\u00f3n full-stack con Leptos (actualizada en esta entrega a <strong>Leptos 0.9.0-rc0<\/strong>): SSR, hidrataci\u00f3n WASM, SQLite y autenticaci\u00f3n con cookies de sesi\u00f3n.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">Nota: Leptos 0.9 todav\u00eda es una versi\u00f3n candidata (<code>0.9.0-rc0<\/code>), as\u00ed que algunas APIs podr\u00edan cambiar hasta la versi\u00f3n final. Si usas la serie estable 0.8, revisa las diferencias antes de copiar el c\u00f3digo.<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">Ten\u00edamos una p\u00e1gina <code>\/users<\/code> protegida con un patr\u00f3n muy com\u00fan en el frontend:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ Redirecci\u00f3n client-side si no hay sesi\u00f3n\nEffect::new(move |_| {\n    if let AuthState::Unauthenticated = auth.state.get() {\n        navigate(\"\/login\", Default::default());\n    }\n});<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Parece que todo est\u00e1 bajo control: si un usuario no autenticado entra a <code>\/users<\/code>, la interfaz lo expulsa a \/login.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>El problema<\/strong> es que cada macro <code>#[server]<\/code> genera un endpoint HTTP p\u00fablico, por ejemplo <code>POST \/api\/delete_user<\/code>. Basta con ejecutar esto en una terminal:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>curl -i -X POST http:\/\/127.0.0.1:3000\/api\/delete_user \\\n  -H \"Content-Type: application\/x-www-form-urlencoded\" \\\n  -d \"id=1\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Y el usuario quedaba eliminado. La funci\u00f3n no comprobaba qui\u00e9n hac\u00eda la llamada ni qu\u00e9 rol ten\u00eda.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">Y el usuario quedaba eliminado. La funci\u00f3n no comprobaba qui\u00e9n hac\u00eda la llamada ni qu\u00e9 rol ten\u00eda.<br>Regla de oro: las server functions no son llamadas a funciones privadas locales, sino endpoints HTTP expuestos al mundo. Proteger la interfaz es UX; la seguridad real ocurre en el servidor.<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">El resto del art\u00edculo recorre las piezas que usamos para cerrar ese hueco: errores tipados, pol\u00edticas compartidas, guards, una capa <code>api<\/code> con flujo est\u00e1ndar por funci\u00f3n, inyecci\u00f3n del pool y tests que atacan la API como lo har\u00eda un cliente externo.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Errores tipados con c\u00f3digos HTTP reales<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Por defecto, los errores en Leptos suelen terminar en un gen\u00e9rico <code>ServerFnError::new(\"Error...\")<\/code>, que devuelve un HTTP 500 con texto plano. As\u00ed el cliente no puede distinguir una sesi\u00f3n expirada de un permiso faltante.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">La soluci\u00f3n es un enum propio, <code>AppError<\/code>, que implementa <code>FromServerFnError<\/code> y asigna a cada variante su c\u00f3digo HTTP:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/domain\/error.rs\nuse http::StatusCode;\nuse leptos::server_fn::codec::JsonEncoding;\nuse leptos::server_fn::error::{FromServerFnError, ServerFnErrorErr};\nuse serde::{Deserialize, Serialize};\n\n#&#91;derive(Debug, Clone, PartialEq, Serialize, Deserialize, thiserror::Error)]\npub enum AppError {\n    #&#91;error(\"Debes iniciar sesi\u00f3n para continuar\")]\n    Unauthorized, \/\/ HTTP 401\n\n    #&#91;error(\"{0}\")]\n    Forbidden(String), \/\/ HTTP 403\n\n    #&#91;error(\"{0}\")]\n    NotFound(String), \/\/ HTTP 404\n\n    #&#91;error(\"{0}\")]\n    Validation(String), \/\/ HTTP 422\n\n    #&#91;error(\"{0}\")]\n    Conflict(String), \/\/ HTTP 409\n\n    #&#91;error(\"Error interno del servidor\")]\n    Internal, \/\/ HTTP 500\n\n    #&#91;error(\"Error de comunicaci\u00f3n con el servidor: {0}\")]\n    ServerFn(String),\n}\n\nimpl AppError {\n    pub fn forbidden(msg: impl Into&lt;String>) -> Self {\n        Self::Forbidden(msg.into())\n    }\n}\n\nimpl FromServerFnError for AppError {\n    type Encoder = JsonEncoding;\n\n    fn from_server_fn_error(value: ServerFnErrorErr) -> Self {\n        AppError::ServerFn(value.to_string())\n    }\n\n    fn status_code(&amp;self) -> StatusCode {\n        match self {\n            AppError::Unauthorized => StatusCode::UNAUTHORIZED,\n            AppError::Forbidden(_) => StatusCode::FORBIDDEN,\n            AppError::NotFound(_) => StatusCode::NOT_FOUND,\n            AppError::Validation(_) => StatusCode::UNPROCESSABLE_ENTITY,\n            AppError::Conflict(_) => StatusCode::CONFLICT,\n            AppError::Internal | AppError::ServerFn(_) => StatusCode::INTERNAL_SERVER_ERROR,\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Con esto, un 401 significa \u00abinicia sesi\u00f3n\u00bb y un 403 significa \u00abest\u00e1s autenticado, pero no tienes permiso\u00bb. Adem\u00e1s, en el servidor implementamos From<a href=\"sqlx::Error\">sqlx::Error<\/a> for AppError para registrar fallos de SQL internamente y devolver siempre AppError::Internal \u2014salvo violaciones UNIQUE, que se traducen en AppError::Conflict (409)\u2014, evitando filtrar detalles del esquema al cliente.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Pol\u00edticas puras en el dominio<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Para no duplicar la l\u00f3gica de \u00abqui\u00e9n puede hacer qu\u00e9\u00bb, la definimos como funciones puras en <code>domain\/policy.rs<\/code>. Al vivir en el dominio isom\u00f3rfico, se comparten entre cliente y servidor:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>El servidor las ejecuta para garantizar la seguridad.<\/li>\n\n\n\n<li><strong>La interfaz (WASM)<\/strong> las ejecuta para ocultar botones o deshabilitar acciones (UX).<\/li>\n<\/ul>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/domain\/policy.rs\npub fn can_manage_users(actor: &amp;User) -> Result&lt;(), AppError> {\n    match actor.role {\n        UserRole::Admin => Ok(()),\n        UserRole::User => Err(AppError::forbidden(\n            \"Solo los administradores pueden gestionar usuarios\",\n        )),\n    }\n}\n\npub fn can_delete_user(actor: &amp;User, target_id: i64) -> Result&lt;(), AppError> {\n    can_manage_users(actor)?;\n    if actor.id == target_id {\n        return Err(AppError::forbidden(\"No puedes eliminar tu propia cuenta\"));\n    }\n    Ok(())\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">En el frontend reutilizamos exactamente la misma funci\u00f3n para adaptar la vista y reaccionamos por variante cuando una llamada falla:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/frontend\/users\/list_page.rs\nlet can_manage = Memo::new(move |_| {\n    auth.user()\n        .map(|u| policy::can_manage_users(&amp;u).is_ok())\n        .unwrap_or(false)\n});\n\n\/\/ Y al renderizar el Resource de usuarios:\nErr(AppError::Unauthorized) => view! {\n    &lt;div class=\"error-state\">\n        &lt;p>\"Tu sesi\u00f3n ha expirado. \"&lt;\/p>\n        &lt;a href=\"\/login\" class=\"btn btn-primary\">\"Iniciar sesi\u00f3n\"&lt;\/a>\n    &lt;\/div>\n}.into_any(),<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Una sola fuente de verdad: si ma\u00f1ana cambia la regla, cambia en un lugar y tanto el bot\u00f3n como el endpoint la respetan.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Guards reutilizables en el servidor<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">En el servidor creamos helpers sencillos que extraen la cookie (<code>HttpOnly<\/code>, <code>SameSite=Lax<\/code> y <code>Secure<\/code> en producci\u00f3n), validan la sesi\u00f3n en SQLite y devuelven el usuario. Cada uno se apoya en el anterior:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/server\/guards.rs\npub async fn current_user() -> Result&lt;Option&lt;User>, AppError> {\n    let Some(session_id) = session::read_cookie().await else {\n        return Ok(None);\n    };\n\n    let pool = db::pool()?;\n    let user = session::find_user(&amp;pool, &amp;session_id).await?;\n    if user.is_none() {\n        session::clear_cookie();\n    }\n    Ok(user)\n}\n\npub async fn require_user() -> Result&lt;User, AppError> {\n    current_user().await?.ok_or(AppError::Unauthorized)\n}\n\npub async fn require_admin() -> Result&lt;User, AppError> {\n    let user = require_user().await?;\n    policy::can_manage_users(&amp;user)?;\n    Ok(user)\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><code>current_user<\/code> no falla si no hay sesi\u00f3n: devuelve <code>None<\/code> (y de paso limpia la cookie del navegador si hab\u00eda caducado). <code>require_user<\/code> convierte esa ausencia en un <code>401<\/code>, y <code>require_admin<\/code> reutiliza la pol\u00edtica del dominio para producir el <code>403<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><code>src\/api<\/code>: la frontera de seguridad y su flujo est\u00e1ndar<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Para que no se mezcle el control de acceso con las consultas SQL, separamos responsabilidades en dos m\u00f3dulos:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>src\/api\/<\/code>: contiene todas las server functions (<code>#[server]<\/code>). Es la \u00fanica puerta de entrada desde el exterior.<\/li>\n\n\n\n<li><code>src\/server\/<\/code>: repositorios puros (<code>users<\/code>, <code>session<\/code>) que asumen que quien los llama ya valid\u00f3 permisos.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Con esa frontera clara, toda server function sigue el mismo orden: autenticar, autorizar y reci\u00e9n entonces ejecutar.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/api\/users.rs\n#&#91;server(prefix = \"\/api\", endpoint = \"delete_user\")]\npub async fn delete_user(id: i64) -> Result&lt;(), AppError> {\n    \/\/ 1. Autenticaci\u00f3n y rol base (401 sin sesi\u00f3n, 403 si no es admin)\n    let actor = guards::require_admin().await?;\n\n    \/\/ 2. Regla espec\u00edfica (403 si intenta borrarse a s\u00ed mismo)\n    policy::can_delete_user(&amp;actor, id)?;\n\n    \/\/ 3. Ejecuci\u00f3n en base de datos\n    let pool = db::pool()?;\n    session::delete_for_user(&amp;pool, id).await?;\n    users::delete(&amp;pool, id).await\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">El mismo <code>curl<\/code> del principio ahora recibe un <code>401<\/code> sin cookie y un <code>403<\/code> con la sesi\u00f3n de un usuario normal. Adem\u00e1s, al eliminar un usuario borramos sus sesiones activas de inmediato. Como el patr\u00f3n es siempre igual, auditar un endpoint nuevo consiste en mirar sus primeras l\u00edneas.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Adi\u00f3s a <code>OnceLock<\/code>: el pool por contexto<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Antes, el pool de base de datos viv\u00eda en un <code>static OnceLock&lt;SqlitePool><\/code>. Un global as\u00ed dificulta las pruebas autom\u00e1ticas, porque todos los tests comparten el mismo estado.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ahora usamos <code>leptos_routes_with_context<\/code> en Axum para inyectar el pool en cada petici\u00f3n (tanto de SSR como de server functions):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/server\/app.rs\npub fn build_router(leptos_options: LeptosOptions, pool: SqlitePool) -> Router {\n    let routes = generate_route_list(App);\n\n    Router::new()\n        .leptos_routes_with_context(\n            &amp;leptos_options,\n            routes,\n            move || provide_context(pool.clone()),\n            {\n                let leptos_options = leptos_options.clone();\n                move || shell(leptos_options.clone())\n            },\n        )\n        .fallback(leptos_axum::file_and_error_handler(shell))\n        .with_state(leptos_options)\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">En cualquier server function basta con llamar a <code>db::pool()<\/code>, que extrae <code>use_context::&lt;SqlitePool>()<\/code>. As\u00ed cada test construye su propio router con una base <code>sqlite::memory:<\/code> aislada (configurada con <code>max_connections(1)<\/code> para que todas las consultas del test compartan esa misma base en memoria).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Rutas protegidas en SSR: redirecci\u00f3n sin parpadeo<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">El <code>Effect<\/code> del principio solo corre en el navegador: primero env\u00eda la p\u00e1gina y espera a que cargue WASM para redirigir. Para que el servidor decida desde el primer momento, <code>AuthContext<\/code> ahora obtiene la sesi\u00f3n con un <code>Resource<\/code> reactivo a las acciones de <code>login<\/code> y <code>logout<\/code>, exponiendo <code>is_authenticated() -> Option&lt;bool><\/code> (<code>None<\/code> mientras carga).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Eso encaja directo con <code>ProtectedRoute<\/code> y <code>SsrMode::Async<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/app.rs\n&lt;ProtectedRoute\n    path=StaticSegment(\"users\")\n    view=UsersListPage\n    condition=move || auth.is_authenticated()\n    redirect_path=|| \"\/login\"\n    ssr=SsrMode::Async\n\/><\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Leptos resuelve el <code>Resource<\/code> de sesi\u00f3n durante el SSR y emite directamente un HTTP <code>302 Found<\/code> a <code>\/login<\/code> si no hay sesi\u00f3n, sin llegar a mostrar la p\u00e1gina. Sigue siendo UX: la seguridad de los datos la garantizan las server functions de <code>src\/api<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Verificaci\u00f3n con tests de integraci\u00f3n reales<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Para confirmar que el hueco est\u00e1 cerrado, los tests atacan directamente el router de Axum con <code>tower::ServiceExt::oneshot<\/code>, emulando a un cliente HTTP externo igual que el <code>curl<\/code> del inicio:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ tests\/authorization.rs\n#&#91;tokio::test]\nasync fn anonymous_cannot_delete_users() {\n    let app = TestApp::new().await;\n    let (status, _, _) = app.post(\"delete_user\", \"id=2\", None).await;\n    assert_eq!(status, StatusCode::UNAUTHORIZED);\n}\n\n#&#91;tokio::test]\nasync fn regular_user_cannot_delete_users() {\n    let app = TestApp::new().await;\n    let cookie = app.login(\"alice\", \"alice123\").await;\n    let (status, _, _) = app.post(\"delete_user\", \"id=1\", Some(&amp;cookie)).await;\n    assert_eq!(status, StatusCode::FORBIDDEN);\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Se ejecutan con <code>cargo test --features ssr<\/code>. En total son 15 tests de integraci\u00f3n (m\u00e1s los unitarios de <code>domain::policy<\/code>) que cubren:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Llamadas an\u00f3nimas (<code>401<\/code>), cookies falsificadas y sesiones expiradas en SQLite.<\/li>\n\n\n\n<li>Usuarios sin permisos intentando borrar o promocionarse a <code>Admin<\/code> (<code>403<\/code>).<\/li>\n\n\n\n<li>Administradores intentando borrarse o quitarse su propio rol (<code>403<\/code>).<\/li>\n\n\n\n<li>Revocaci\u00f3n inmediata de sesiones al hacer logout o al eliminar un usuario.<\/li>\n\n\n\n<li>Respuesta id\u00e9ntica (<code>422<\/code>) y verificaci\u00f3n con hash <em>dummy<\/em> en login para evitar enumerar usuarios por mensaje o tiempo de respuesta.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Hasta donde hemos llegado<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La lecci\u00f3n central cabe en una l\u00ednea: cada <code>#[server]<\/code> es una puerta p\u00fablica, as\u00ed que cada una necesita su propio control de acceso. El patr\u00f3n que usamos lo resume:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Errores tipados<\/strong> (<code>AppError<\/code>) para que el cliente distinga <code>401<\/code>, <code>403<\/code>, <code>409<\/code> y <code>422<\/code> sin filtrar errores internos.<\/li>\n\n\n\n<li><strong>Pol\u00edticas puras<\/strong> en el dominio, compartidas por servidor (seguridad) e interfaz (UX).<\/li>\n\n\n\n<li><strong>Capa<\/strong> <code>api<\/code> <strong>+ guards<\/strong>, aplicando el mismo flujo al inicio de cada funci\u00f3n.<\/li>\n\n\n\n<li><strong>Pool inyectado<\/strong> por contexto, eliminando globales para poder aislar cada prueba.<\/li>\n\n\n\n<li><strong>Tests HTTP<\/strong> reales contra el router, pensando como un cliente externo y no desde la UI.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Puedes revisar y ejecutar todo el c\u00f3digo en la rama <a href=\"https:\/\/github.com\/jure-ve\/user-leptos-app\/tree\/feat\/autorizacion-server-functions\" target=\"_blank\" rel=\"noopener\"><code>feat\/autorizacion-server-functions<\/code><\/a> de <code>user-leptos-app<\/code>. Si ves un caso que los tests no cubren, cu\u00e9ntamelo en los comentarios.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Sigamos codificando.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Redirigir al login en la interfaz no protege nada: cada #[server] de Leptos es un endpoint HTTP p\u00fablico, y la autorizaci\u00f3n real tiene que vivir en el servidor. En este art\u00edculo veremos c\u00f3mo blindar una aplicaci\u00f3n full-stack con Leptos y Axum, con c\u00f3digo que puedes revisar en la rama feat\/autorizacion-server-functions de user-leptos-app. La ilusi\u00f3n de [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_piko_noindex":"","footnotes":""},"categories":[7],"tags":[10],"class_list":["post-371","post","type-post","status-publish","format-standard","hentry","category-desarrollo","tag-rust"],"_links":{"self":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/371","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/comments?post=371"}],"version-history":[{"count":0,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/371\/revisions"}],"wp:attachment":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/media?parent=371"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/categories?post=371"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/tags?post=371"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}