diff --git a/packages/api/src/api/rest/etape-creer.test.integration.ts b/packages/api/src/api/rest/etape-creer.test.integration.ts
index 93d9b9dc7263f614f8f87be734a1f1f8d8fc83ba..0fbb6a09707d7c77085cf5f1b42b4e2374171fef 100644
--- a/packages/api/src/api/rest/etape-creer.test.integration.ts
+++ b/packages/api/src/api/rest/etape-creer.test.integration.ts
@@ -176,7 +176,7 @@ describe('etapeCreer', () => {
     expect(result.body).toMatchInlineSnapshot(`
       {
         "detail": "La valeur du champ "date" est invalide.",
-        "message": "Problème de validation de données",
+        "message": "Corps de la requête manquant ou invalide",
         "status": 400,
         "zodErrorReadableMessage": "Validation error: Invalid at "date"; date invalide at "date"; Required at "duree"; Required at "dateDebut"; Required at "dateFin"; Required at "substances"; Required at "geojson4326Perimetre"; Required at "geojson4326Points"; Required at "geojsonOriginePoints"; Required at "geojsonOriginePerimetre"; Required at "geojsonOrigineForages"; Required at "geojsonOrigineGeoSystemeId"; Required at "titulaireIds"; Required at "amodiataireIds"; Required at "note"; Required at "contenu"; Required at "heritageProps"; Required at "heritageContenu"; Required at "etapeDocuments"; Required at "entrepriseDocumentIds"; Required at "etapeAvis"",
       }
diff --git a/packages/api/src/api/rest/etape-modifier.test.integration.ts b/packages/api/src/api/rest/etape-modifier.test.integration.ts
index cb95b86179a809c5f67f91d9be5aad16aa7039a0..fa59f834871841dd5f07ac3206bd8471842a1dbe 100644
--- a/packages/api/src/api/rest/etape-modifier.test.integration.ts
+++ b/packages/api/src/api/rest/etape-modifier.test.integration.ts
@@ -75,7 +75,7 @@ describe('etapeModifier', () => {
     expect(result.body).toMatchInlineSnapshot(`
       {
         "detail": "La valeur du champ "date" est invalide.",
-        "message": "Problème de validation de données",
+        "message": "Corps de la requête invalide",
         "status": 400,
         "zodErrorReadableMessage": "Validation error: Invalid at "date"; date invalide at "date"; Required at "duree"; Required at "dateDebut"; Required at "dateFin"; Required at "substances"; Required at "geojson4326Perimetre"; Required at "geojson4326Points"; Required at "geojsonOriginePoints"; Required at "geojsonOriginePerimetre"; Required at "geojsonOrigineForages"; Required at "geojsonOrigineGeoSystemeId"; Required at "titulaireIds"; Required at "amodiataireIds"; Required at "note"; Required at "contenu"; Required at "titreDemarcheId"; Required at "heritageProps"; Required at "heritageContenu"; Required at "etapeDocuments"; Required at "entrepriseDocumentIds"; Required at "etapeAvis"",
       }
diff --git a/packages/api/src/api/rest/etapes.test.integration.ts b/packages/api/src/api/rest/etapes.test.integration.ts
index 26487e6eeca311709d4ec1392db2ad857d41004c..f68ea39b823b658d722459346fc485d530bd1936 100644
--- a/packages/api/src/api/rest/etapes.test.integration.ts
+++ b/packages/api/src/api/rest/etapes.test.integration.ts
@@ -1,6 +1,6 @@
 import { dbManager } from '../../../tests/db-manager'
 import { userSuper } from '../../database/user-super'
-import { restCall, restDeleteCall, restNewCall, restNewPostCall } from '../../../tests/_utils/index'
+import { restCall, restNewCall, restNewDeleteCall, restNewPostCall } from '../../../tests/_utils/index'
 import { caminoDateValidator, dateAddDays, toCaminoDate } from 'camino-common/src/date'
 import { afterAll, beforeAll, test, expect, describe, vi } from 'vitest'
 import type { Pool } from 'pg'
@@ -284,14 +284,14 @@ describe('etapeSupprimer', () => {
       ],
     })
 
-    const tested = await restDeleteCall(
+    const tested = await restNewDeleteCall(
       dbPool,
       '/rest/etapes/:etapeIdOrSlug',
       { etapeIdOrSlug: etapeId },
       role && isAdministrationRole(role) ? { role, administrationId: 'min-mctrct-dgcl-01' } : undefined
     )
 
-    expect(tested.statusCode).toBe(HTTP_STATUS.FORBIDDEN)
+    expect(tested.statusCode, JSON.stringify(tested.body)).toBe(HTTP_STATUS.FORBIDDEN)
   })
 
   test('peut supprimer une étape (utilisateur super)', async () => {
@@ -324,7 +324,7 @@ describe('etapeSupprimer', () => {
       ],
     })
 
-    const tested = await restDeleteCall(dbPool, '/rest/etapes/:etapeIdOrSlug', { etapeIdOrSlug: etapeId }, userSuper)
+    const tested = await restNewDeleteCall(dbPool, '/rest/etapes/:etapeIdOrSlug', { etapeIdOrSlug: etapeId }, userSuper)
 
     expect(tested.statusCode).toBe(HTTP_STATUS.NO_CONTENT)
   })
@@ -380,7 +380,7 @@ describe('etapeSupprimer', () => {
     const getEtape = await restCall(dbPool, '/rest/titres/:titreId', { titreId: titreId }, user)
     expect(getEtape.statusCode).toBe(HTTP_STATUS.OK)
 
-    const tested = await restDeleteCall(dbPool, '/rest/etapes/:etapeIdOrSlug', { etapeIdOrSlug: etapeId }, user)
+    const tested = await restNewDeleteCall(dbPool, '/rest/etapes/:etapeIdOrSlug', { etapeIdOrSlug: etapeId }, user)
 
     expect(tested.statusCode).toBe(HTTP_STATUS.FORBIDDEN)
   })
diff --git a/packages/api/src/api/rest/etapes.ts b/packages/api/src/api/rest/etapes.ts
index 46487ac625e747f3c1a3afbb28f812096cb12d17..209ab488291c96afa0226dfb6d3530749c196e52 100644
--- a/packages/api/src/api/rest/etapes.ts
+++ b/packages/api/src/api/rest/etapes.ts
@@ -1,11 +1,8 @@
-import { CaminoRequest, CustomResponse } from './express-type'
 import {
   EtapeTypeEtapeStatutWithMainStep,
-  etapeIdValidator,
   EtapeId,
   GetEtapeDocumentsByEtapeId,
   ETAPE_IS_NOT_BROUILLON,
-  etapeIdOrSlugValidator,
   GetEtapeAvisByEtapeId,
   EtapeBrouillon,
   etapeSlugValidator,
@@ -64,13 +61,14 @@ import { KM2 } from 'camino-common/src/number'
 import { FeatureMultiPolygon, FeatureCollectionPoints } from 'camino-common/src/perimetre'
 import { canHaveForages } from 'camino-common/src/permissions/titres'
 import { SecteursMaritimes, getSecteurMaritime } from 'camino-common/src/static/facades'
-import { callAndExit, shortCircuitError, zodParseEffect } from '../../tools/fp-tools'
-import { RestNewGetCall, RestNewPostCall, RestNewPutCall } from '../../server/rest'
-import { Effect, Match } from 'effect'
+import { shortCircuitError } from '../../tools/fp-tools'
+import { RestNewDeleteCall, RestNewGetCall, RestNewPostCall, RestNewPutCall } from '../../server/rest'
+import { Effect, Match, Option } from 'effect'
 import { EffectDbQueryAndValidateErrors } from '../../pg-database'
 import { CaminoError } from 'camino-common/src/zod-tools'
 import { machineFind } from '../../business/rules-demarches/machines'
 import { TitreEtapeForMachine } from '../../business/rules-demarches/machine-common'
+import { newEtapeId } from '../../database/models/_format/id-create'
 
 type GetEtapeEntrepriseDocumentsErrors = EffectDbQueryAndValidateErrors
 export const getEtapeEntrepriseDocuments: RestNewGetCall<'/rest/etapes/:etapeId/entrepriseDocuments'> = (
@@ -179,114 +177,239 @@ export const getEtapeAvis: RestNewGetCall<'/rest/etapes/:etapeId/etapeAvis'> = (
     )
   )
 
-export const deleteEtape =
-  (pool: Pool) =>
-  async (req: CaminoRequest, res: CustomResponse<void>): Promise<void> => {
-    const user = req.auth
-
-    const etapeId = etapeIdOrSlugValidator.safeParse(req.params.etapeIdOrSlug)
-    if (!etapeId.success) {
-      res.sendStatus(HTTP_STATUS.BAD_REQUEST)
-    } else if (isNullOrUndefined(user)) {
-      res.sendStatus(HTTP_STATUS.NOT_FOUND)
-    } else {
-      try {
-        const titreEtape = await titreEtapeGet(
-          etapeId.data,
-          {
-            fields: {
-              demarche: { titre: { pointsEtape: { id: {} } } },
-            },
-          },
-          user
+const etapeFetchError = `Une erreur s'est produite lors de la récupération de l'étape` as const
+const etapeIntrouvableError = `L'étape n'existe pas` as const
+const etapeIncompletError = `L'étape n'a pas été chargée de manière complète` as const
+const droitsManquantsError = `Les droits d'accès à cette étape ne sont pas suffisants` as const
+const etapeUpdateFail = `La mise à jour de l'étape a échoué` as const
+const etapeUpdateTaskFail = `Les tâches après mise à jour de l'étape ont échoué` as const
+const demarcheFetchError = `Une erreur s'est produite lors de la récupération de la démarche` as const
+const demarcheIntrouvableError = `La démarche associée à l'étape est vide` as const
+const titreIntrouvableError = `Le titre associé à l'étape est vide` as const
+const demarcheInvalideError = `La suppression de cette étape mène à une démarche invalide` as const
+type DeleteEtapeErrors =
+  | EffectDbQueryAndValidateErrors
+  | typeof etapeFetchError
+  | typeof etapeIntrouvableError
+  | typeof etapeIncompletError
+  | typeof droitsManquantsError
+  | typeof etapeUpdateFail
+  | typeof etapeUpdateTaskFail
+  | typeof demarcheFetchError
+  | typeof demarcheIntrouvableError
+  | typeof titreIntrouvableError
+  | typeof demarcheInvalideError
+export const deleteEtape: RestNewDeleteCall<'/rest/etapes/:etapeIdOrSlug'> = (rootPipe): Effect.Effect<Option.Option<never>, CaminoApiError<DeleteEtapeErrors>> =>
+  rootPipe.pipe(
+    Effect.bind('etape', ({ params, user }) =>
+      Effect.Do.pipe(
+        () =>
+          Effect.tryPromise({
+            try: () =>
+              titreEtapeGet(
+                params.etapeIdOrSlug,
+                {
+                  fields: {
+                    demarche: { titre: { pointsEtape: { id: {} } } },
+                  },
+                },
+                user
+              ),
+            catch: error => ({ message: etapeFetchError, extra: error }),
+          }),
+        Effect.filterOrFail(
+          (etape): etape is ITitreEtape => isNotNullNorUndefined(etape),
+          () => ({ message: etapeIntrouvableError })
         )
-
-        if (isNullOrUndefined(titreEtape)) {
-          res.sendStatus(HTTP_STATUS.NOT_FOUND)
-        } else {
-          if (!titreEtape.demarche || !titreEtape.demarche.titre || titreEtape.demarche.titre.administrationsLocales === undefined) {
-            throw new Error('la démarche n’est pas chargée complètement')
-          }
-
-          if (
-            !canDeleteEtape(user, titreEtape.typeId, titreEtape.isBrouillon, titreEtape.titulaireIds ?? [], titreEtape.demarche.titre.administrationsLocales ?? [], titreEtape.demarche.typeId, {
-              typeId: titreEtape.demarche.titre.typeId,
-              titreStatutId: titreEtape.demarche.titre.titreStatutId,
-            })
-          ) {
-            res.sendStatus(HTTP_STATUS.FORBIDDEN)
-          } else {
-            const titreDemarche = await titreDemarcheGet(
-              titreEtape.titreDemarcheId,
-              {
-                fields: {
-                  titre: {
-                    demarches: { etapes: { id: {} } },
+      )
+    ),
+    Effect.bind('demarche', ({ etape }) =>
+      Effect.Do.pipe(
+        Effect.map(() => etape.demarche),
+        Effect.filterOrFail(
+          (demarche): demarche is ITitreDemarche => isNotNullNorUndefined(demarche),
+          () => ({ message: etapeIncompletError, detail: 'demarche est manquant' })
+        )
+      )
+    ),
+    Effect.bind('titre', ({ demarche }) =>
+      Effect.Do.pipe(
+        Effect.map(() => demarche.titre),
+        Effect.filterOrFail(
+          (titre): titre is ITitre => isNotNullNorUndefined(titre) && titre.administrationsLocales !== undefined,
+          () => ({ message: etapeIncompletError, detail: 'titre est manquant ou incomplet' })
+        )
+      )
+    ),
+    Effect.filterOrFail(
+      ({ titre, demarche, etape, user }) =>
+        canDeleteEtape(user, etape.typeId, etape.isBrouillon, etape.titulaireIds ?? [], titre.administrationsLocales ?? [], demarche.typeId, {
+          typeId: titre.typeId,
+          titreStatutId: titre.titreStatutId,
+        }),
+      () => ({ message: droitsManquantsError })
+    ),
+    Effect.bind('fullDemarche', ({ etape }) =>
+      Effect.Do.pipe(
+        () =>
+          Effect.tryPromise({
+            try: () =>
+              titreDemarcheGet(
+                etape.titreDemarcheId,
+                {
+                  fields: {
+                    titre: {
+                      demarches: { etapes: { id: {} } },
+                    },
+                    etapes: { id: {} },
                   },
-                  etapes: { id: {} },
                 },
-              },
-              userSuper
-            )
-
-            if (!titreDemarche) throw new Error("la démarche n'existe pas")
-
-            if (!titreDemarche.titre) throw new Error("le titre n'existe pas")
-
-            const { valid, errors: rulesErrors } = titreDemarcheUpdatedEtatValidate(titreDemarche.typeId, titreDemarche.titre, titreEtape, titreDemarche.id, titreDemarche.etapes!, true)
-
-            if (!valid) {
-              throw new Error(rulesErrors.join(', '))
-            }
-            await titreEtapeUpdate(titreEtape.id, { archive: true }, user, titreDemarche.titreId)
-
-            await titreEtapeUpdateTask(pool, null, titreEtape.titreDemarcheId, user)
-
-            res.sendStatus(HTTP_STATUS.NO_CONTENT)
-          }
-        }
-      } catch (e) {
-        res.sendStatus(HTTP_STATUS.INTERNAL_SERVER_ERROR)
-        console.error(e)
+                userSuper
+              ),
+            catch: error => ({ message: demarcheFetchError, extra: error }),
+          }),
+        Effect.filterOrFail(
+          (demarche): demarche is ITitreDemarche => isNotNullNorUndefined(demarche),
+          () => ({ message: demarcheIntrouvableError })
+        )
+      )
+    ),
+    Effect.bind('fullTitre', ({ fullDemarche }) =>
+      Effect.Do.pipe(
+        Effect.map(() => fullDemarche.titre),
+        Effect.filterOrFail(
+          (titre): titre is ITitre => isNotNullNorUndefined(titre),
+          () => ({ message: titreIntrouvableError, detail: 'titre est manquant ou incomplet' })
+        )
+      )
+    ),
+    Effect.tap(({ fullDemarche, fullTitre, etape }) => {
+      const { valid, errors: rulesErrors } = titreDemarcheUpdatedEtatValidate(fullDemarche.typeId, fullTitre, etape, fullDemarche.id, fullDemarche.etapes!, true)
+      if (!valid) {
+        return Effect.fail({ message: demarcheInvalideError, detail: rulesErrors.join(', ') })
       }
-    }
-  }
-export const getEtape =
-  (_pool: Pool) =>
-  async (req: CaminoRequest, res: CustomResponse<FlattenEtape>): Promise<void> => {
-    const user = req.auth
-
-    const etapeId = etapeIdOrSlugValidator.safeParse(req.params.etapeIdOrSlug)
-    if (!etapeId.success) {
-      res.sendStatus(HTTP_STATUS.BAD_REQUEST)
-    } else if (isNullOrUndefined(user)) {
-      res.sendStatus(HTTP_STATUS.FORBIDDEN)
-    } else {
-      try {
-        const titreEtape = await titreEtapeGet(etapeId.data, { fields: { demarche: { titre: { pointsEtape: { id: {} } } } }, fetchHeritage: true }, user)
 
-        if (isNullOrUndefined(titreEtape)) {
-          res.sendStatus(HTTP_STATUS.NOT_FOUND)
-        } else if (isNullOrUndefined(titreEtape.titulaireIds) || isNullOrUndefined(titreEtape.demarche?.titre) || titreEtape.demarche.titre.administrationsLocales === undefined) {
-          console.error('la démarche n’est pas chargée complètement')
-          res.sendStatus(HTTP_STATUS.INTERNAL_SERVER_ERROR)
-        } else if (
-          !canEditEtape(user, titreEtape.typeId, titreEtape.isBrouillon, titreEtape.titulaireIds ?? [], titreEtape.demarche.titre.administrationsLocales ?? [], titreEtape.demarche.typeId, {
-            typeId: titreEtape.demarche.titre.typeId,
-            titreStatutId: titreEtape.demarche.titre.titreStatutId,
+      return Effect.succeed(true)
+    }),
+    Effect.tap(({ etape, fullDemarche, user }) =>
+      Effect.tryPromise({
+        try: () => titreEtapeUpdate(etape.id, { archive: true }, user, fullDemarche.titreId),
+        catch: error => ({ message: etapeUpdateFail, extra: error }),
+      })
+    ),
+    Effect.tap(({ pool, etape, user }) =>
+      Effect.tryPromise({
+        try: () => titreEtapeUpdateTask(pool, null, etape.titreDemarcheId, user),
+        catch: error => ({ message: etapeUpdateTaskFail, extra: error }),
+      })
+    ),
+    Effect.map(() => Option.none()),
+    Effect.mapError(caminoError =>
+      Match.value(caminoError.message).pipe(
+        Match.when("L'étape n'existe pas", () => ({
+          ...caminoError,
+          status: HTTP_STATUS.NOT_FOUND,
+        })),
+        Match.when("Les droits d'accès à cette étape ne sont pas suffisants", () => ({
+          ...caminoError,
+          status: HTTP_STATUS.FORBIDDEN,
+        })),
+        Match.when('La suppression de cette étape mène à une démarche invalide', () => ({
+          ...caminoError,
+          status: HTTP_STATUS.BAD_REQUEST,
+        })),
+        Match.whenOr(
+          "L'étape n'a pas été chargée de manière complète",
+          "La démarche associée à l'étape est vide",
+          "La mise à jour de l'étape a échoué",
+          "Le titre associé à l'étape est vide",
+          "Les tâches après mise à jour de l'étape ont échoué",
+          "Une erreur s'est produite lors de la récupération de l'étape",
+          "Une erreur s'est produite lors de la récupération de la démarche",
+          () => ({
+            ...caminoError,
+            status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
           })
-        ) {
-          res.sendStatus(HTTP_STATUS.FORBIDDEN)
-        } else {
-          res.json(await callAndExit(iTitreEtapeToFlattenEtape(titreEtape)))
-        }
-      } catch (e) {
-        console.error(e)
+        ),
+        Match.exhaustive
+      )
+    )
+  )
 
-        res.sendStatus(HTTP_STATUS.INTERNAL_SERVER_ERROR)
-      }
-    }
-  }
+type GetEtapeErrors = EffectDbQueryAndValidateErrors | TitreEtapeToFlattenEtapeErrors | typeof etapeFetchError | typeof etapeIntrouvableError | typeof etapeIncompletError | typeof droitsManquantsError
+export const getEtape: RestNewGetCall<'/rest/etapes/:etapeIdOrSlug'> = (rootPipe): Effect.Effect<FlattenEtape, CaminoApiError<GetEtapeErrors>> =>
+  rootPipe.pipe(
+    Effect.bind('etape', ({ user, params }) =>
+      Effect.Do.pipe(
+        () =>
+          Effect.tryPromise({
+            try: () => titreEtapeGet(params.etapeIdOrSlug, { fields: { demarche: { titre: { pointsEtape: { id: {} } } } }, fetchHeritage: true }, user),
+            catch: error => ({ message: etapeFetchError, extra: error }),
+          }),
+        Effect.filterOrFail(
+          (etape): etape is ITitreEtape => isNotNullNorUndefined(etape),
+          () => ({ message: etapeIntrouvableError })
+        ),
+        Effect.filterOrFail(
+          etape => isNotNullNorUndefined(etape.titulaireIds),
+          () => ({ message: etapeIncompletError, detail: 'titulaireIds est manquant' })
+        )
+      )
+    ),
+    Effect.bind('demarche', ({ etape }) =>
+      Effect.Do.pipe(
+        Effect.map(() => etape.demarche),
+        Effect.filterOrFail(
+          (demarche): demarche is ITitreDemarche => isNotNullNorUndefined(demarche),
+          () => ({ message: etapeIncompletError, detail: 'demarche est manquant' })
+        )
+      )
+    ),
+    Effect.bind('titre', ({ demarche }) =>
+      Effect.Do.pipe(
+        Effect.map(() => demarche.titre),
+        Effect.filterOrFail(
+          (titre): titre is ITitre => isNotNullNorUndefined(titre) && titre.administrationsLocales !== undefined,
+          () => ({ message: etapeIncompletError, detail: 'titre est manquant ou incomplet' })
+        )
+      )
+    ),
+    Effect.filterOrFail(
+      ({ titre, demarche, etape, user }) =>
+        canEditEtape(user, etape.typeId, etape.isBrouillon, etape.titulaireIds ?? [], titre.administrationsLocales ?? [], demarche.typeId, {
+          typeId: titre.typeId,
+          titreStatutId: titre.titreStatutId,
+        }),
+      () => ({ message: droitsManquantsError })
+    ),
+    Effect.flatMap(({ etape }) => iTitreEtapeToFlattenEtape(etape)),
+    Effect.mapError(caminoError =>
+      Match.value(caminoError.message).pipe(
+        Match.when("L'étape n'existe pas", () => ({
+          ...caminoError,
+          status: HTTP_STATUS.NOT_FOUND,
+        })),
+        Match.when("Les droits d'accès à cette étape ne sont pas suffisants", () => ({
+          ...caminoError,
+          status: HTTP_STATUS.FORBIDDEN,
+        })),
+        Match.whenOr(
+          "L'étape n'a pas été chargée de manière complète",
+          'Problème de validation de données',
+          "Une erreur s'est produite lors de la récupération de l'étape",
+          "pas d'héritage chargé",
+          'pas de démarche chargée',
+          'pas de démarche ou de titre chargé',
+          'pas de slug',
+          () => ({
+            ...caminoError,
+            status: HTTP_STATUS.INTERNAL_SERVER_ERROR,
+          })
+        ),
+        Match.exhaustive
+      )
+    )
+  )
 
 const documentDEntrepriseIncorrects = "document d'entreprise incorrects" as const
 type ValidateAndGetEntrepriseDocumentsErrors = typeof documentDEntrepriseIncorrects | EffectDbQueryAndValidateErrors
@@ -442,7 +565,7 @@ const getFlattenEtape = (
       }, {})
       return Effect.succeed(heritageContenu)
     }),
-    Effect.bind('fakeEtapeId', () => zodParseEffect(etapeIdValidator, 'newId')),
+    Effect.let('fakeEtapeId', () => newEtapeId('newId')),
     Effect.bind('flattenEtape', ({ perimetreInfos, heritageProps, heritageContenu, fakeEtapeId }) =>
       iTitreEtapeToFlattenEtape({
         ...etape,
diff --git a/packages/api/src/server/rest.ts b/packages/api/src/server/rest.ts
index 891111944144f516ff3a462f005b7b85e39e0aa3..40264ccf364f628d9da823ebce521401c55af631 100644
--- a/packages/api/src/server/rest.ts
+++ b/packages/api/src/server/rest.ts
@@ -56,7 +56,7 @@ import { titreDemandeCreer } from '../api/rest/titre-demande'
 import { config } from '../config/index'
 import { addLog } from '../api/rest/logs.queries'
 import { HTTP_STATUS } from 'camino-common/src/http'
-import { zodParseEffect } from '../tools/fp-tools'
+import { zodParseEffectTyped } from '../tools/fp-tools'
 import { Cause, Effect, Exit, Option, pipe } from 'effect'
 
 interface IRestResolverResult {
@@ -129,7 +129,7 @@ export type RestNewDeleteCall<Route extends NewDeleteRestRoutes> = (
   data: Effect.Effect<
     {
       pool: Pool
-      user: User
+      user: UserNotNull
       params: z.infer<CaminoRestRoutesType[Route]['params']>
       cookie: CookieParams
     },
@@ -232,7 +232,7 @@ const restRouteImplementations: Readonly<{ [key in CaminoRestRoute]: Transform<k
   },
   '/rest/demarches/:demarcheId/geojson': { newGetCall: getPerimetreInfosByDemarche, ...CaminoRestRoutes['/rest/demarches/:demarcheId/geojson'] },
   '/rest/etapes/:etapeId/geojson': { newGetCall: getPerimetreInfosByEtape, ...CaminoRestRoutes['/rest/etapes/:etapeId/geojson'] },
-  '/rest/etapes/:etapeIdOrSlug': { deleteCall: deleteEtape, getCall: getEtape, ...CaminoRestRoutes['/rest/etapes/:etapeIdOrSlug'] },
+  '/rest/etapes/:etapeIdOrSlug': { newDeleteCall: deleteEtape, newGetCall: getEtape, ...CaminoRestRoutes['/rest/etapes/:etapeIdOrSlug'] },
   '/rest/etapes': { newPostCall: createEtape, newPutCall: updateEtape, ...CaminoRestRoutes['/rest/etapes'] },
   '/rest/etapes/:etapeId/depot': { newPutCall: deposeEtape, ...CaminoRestRoutes['/rest/etapes/:etapeId/depot'] },
   '/rest/etapes/:etapeId/entrepriseDocuments': { newGetCall: getEtapeEntrepriseDocuments, ...CaminoRestRoutes['/rest/etapes/:etapeId/entrepriseDocuments'] },
@@ -265,12 +265,12 @@ export const restWithPool = (dbPool: Pool): Router => {
             const call = Effect.Do.pipe(
               Effect.bind('searchParams', () => {
                 if ('searchParams' in maRoute.newGet) {
-                  return zodParseEffect(maRoute.newGet.searchParams, req.query)
+                  return zodParseEffectTyped(maRoute.newGet.searchParams, req.query, 'Paramètres de recherche manquants ou invalides' as const)
                 }
 
                 return Effect.succeed(undefined)
               }),
-              Effect.bind('params', () => zodParseEffect(maRoute.params, req.params)),
+              Effect.bind('params', () => zodParseEffectTyped(maRoute.params, req.params, 'Paramètres manquants ou invalides')),
               Effect.mapError(caminoError => {
                 return { ...caminoError, status: HTTP_STATUS.BAD_REQUEST }
               }),
@@ -291,7 +291,7 @@ export const restWithPool = (dbPool: Pool): Router => {
               }),
               Effect.bind('parsedResult', ({ result }) =>
                 pipe(
-                  zodParseEffect(maRoute.newGet.output, result),
+                  zodParseEffectTyped(maRoute.newGet.output, result, 'Résultat de la requête invalide'),
                   Effect.mapError(caminoError => ({ ...caminoError, status: HTTP_STATUS.INTERNAL_SERVER_ERROR }))
                 )
               ),
@@ -342,8 +342,8 @@ export const restWithPool = (dbPool: Pool): Router => {
                   return Effect.fail({ message: 'Accès interdit', status: HTTP_STATUS.FORBIDDEN })
                 }
               }),
-              Effect.bind('body', () => zodParseEffect(maRoute.newPost.input, req.body)),
-              Effect.bind('params', () => zodParseEffect(maRoute.params, req.params)),
+              Effect.bind('body', () => zodParseEffectTyped(maRoute.newPost.input, req.body, 'Corps de la requête manquant ou invalide')),
+              Effect.bind('params', () => zodParseEffectTyped(maRoute.params, req.params, 'Paramètres manquants ou invalides')),
               Effect.mapError(caminoError => {
                 if (!('status' in caminoError)) {
                   return { ...caminoError, status: HTTP_STATUS.BAD_REQUEST }
@@ -364,7 +364,7 @@ export const restWithPool = (dbPool: Pool): Router => {
               ),
               Effect.bind('parsedResult', ({ result }) =>
                 pipe(
-                  zodParseEffect(maRoute.newPost.output, result),
+                  zodParseEffectTyped(maRoute.newPost.output, result, 'Résultat de la requête invalide'),
                   Effect.mapError(caminoError => ({ ...caminoError, status: HTTP_STATUS.INTERNAL_SERVER_ERROR }))
                 )
               ),
@@ -413,8 +413,8 @@ export const restWithPool = (dbPool: Pool): Router => {
                   return Effect.fail({ message: 'Accès interdit', status: HTTP_STATUS.FORBIDDEN })
                 }
               }),
-              Effect.bind('body', () => zodParseEffect(maRoute.newPut.input, req.body)),
-              Effect.bind('params', () => zodParseEffect(maRoute.params, req.params)),
+              Effect.bind('body', () => zodParseEffectTyped(maRoute.newPut.input, req.body, 'Corps de la requête invalide')),
+              Effect.bind('params', () => zodParseEffectTyped(maRoute.params, req.params, 'Paramètres manquants ou invalides')),
               Effect.mapError(caminoError => {
                 if (!('status' in caminoError)) {
                   return { ...caminoError, status: HTTP_STATUS.BAD_REQUEST }
@@ -435,7 +435,7 @@ export const restWithPool = (dbPool: Pool): Router => {
               ),
               Effect.bind('parsedResult', ({ result }) =>
                 pipe(
-                  zodParseEffect(maRoute.newPut.output, result),
+                  zodParseEffectTyped(maRoute.newPut.output, result, 'Résultat de la requête invalide'),
                   Effect.mapError(caminoError => ({ ...caminoError, status: HTTP_STATUS.INTERNAL_SERVER_ERROR }))
                 )
               ),
@@ -486,15 +486,27 @@ export const restWithPool = (dbPool: Pool): Router => {
         rest.delete(route, async (req: CaminoRequest, res: express.Response, _next: express.NextFunction) => {
           try {
             const call = Effect.Do.pipe(
-              Effect.bind('params', () => zodParseEffect(maRoute.params, req.params)),
+              Effect.bind('user', () => {
+                if (isNotNullNorUndefined(req.auth)) {
+                  return Effect.succeed(req.auth as UserNotNull)
+                } else {
+                  return Effect.fail({ message: 'Accès interdit', status: HTTP_STATUS.FORBIDDEN })
+                }
+              }),
+              // TODO 2025-04-02 ici, si on ne met pas les params à any, on se retrouve avec une typescript union hell qui fait tout planter
+              Effect.bind('params', () => zodParseEffectTyped(maRoute.params, req.params as any, 'Paramètres manquants ou invalides')),
               Effect.mapError(caminoError => {
-                return { ...caminoError, status: HTTP_STATUS.BAD_REQUEST }
+                if (!('status' in caminoError)) {
+                  return { ...caminoError, status: HTTP_STATUS.BAD_REQUEST }
+                }
+
+                return caminoError
               }),
               // TODO 2024-06-26 ici, si on ne met pas les params et les searchParams à any, on se retrouve avec une typescript union hell qui fait tout planter
-              Effect.bind<'result', { params: any }, Option.Option<never>, CaminoApiError<string>, never>('result', ({ params }) => {
+              Effect.bind<'result', { params: any; user: UserNotNull }, Option.Option<never>, CaminoApiError<string>, never>('result', ({ params, user }) => {
                 return maRoute.newDeleteCall(
                   Effect.Do.pipe(
-                    Effect.let('user', () => req.auth),
+                    Effect.let('user', () => user),
                     Effect.let('pool', () => dbPool),
                     Effect.let('params', () => params),
                     Effect.let('cookie', () => ({
diff --git a/packages/common/src/rest.ts b/packages/common/src/rest.ts
index c5e809b9600dc3203f33cc0a319e4f5ad2a440f6..193ce0d151eb99b60311ca3abce8f071ee0c7090 100644
--- a/packages/common/src/rest.ts
+++ b/packages/common/src/rest.ts
@@ -221,7 +221,7 @@ export const CaminoRestRoutes = {
   '/rest/etapes/:etapeId/etapeDocuments': { params: etapeIdParamsValidator, newGet: { output: getEtapeDocumentsByEtapeIdValidator } },
   '/rest/etapes/:etapeId/etapeAvis': { params: etapeIdParamsValidator, newGet: { output: getEtapeAvisByEtapeIdValidator } },
   '/rest/etapes/:etapeId/entrepriseDocuments': { params: etapeIdParamsValidator, newGet: { output: z.array(etapeEntrepriseDocumentValidator) } },
-  '/rest/etapes/:etapeIdOrSlug': { params: z.object({ etapeIdOrSlug: etapeIdOrSlugValidator }), delete: true, get: { output: flattenEtapeValidator } },
+  '/rest/etapes/:etapeIdOrSlug': { params: z.object({ etapeIdOrSlug: etapeIdOrSlugValidator }), newDelete: true, newGet: { output: flattenEtapeValidator } },
   '/rest/etapes/:etapeId/depot': { params: etapeIdParamsValidator, newPut: { input: z.object({}), output: z.object({ id: etapeIdValidator }) } },
   '/rest/etapes': {
     params: noParamsValidator,
diff --git a/packages/ui/src/api/client-rest.ts b/packages/ui/src/api/client-rest.ts
index 529076e74836ee5ffde7dcfabf47351e64577ec1..b5300bfec0ec49cdddb48aa6e5fac7d7a10fe143 100644
--- a/packages/ui/src/api/client-rest.ts
+++ b/packages/ui/src/api/client-rest.ts
@@ -190,7 +190,7 @@ export const newDeleteWithJson = async <T extends NewDeleteRestRoutes>(path: T,
 }
 
 /**
- * @deprecated use deleteWithJson
+ * @deprecated use newDeleteWithJson
  **/
 export const deleteWithJson = async <T extends DeleteRestRoutes>(path: T, params: CaminoRestParams<T>, searchParams: Record<string, string | string[]> = {}): Promise<void> =>
   callFetch(path, params, 'delete', searchParams)
diff --git a/packages/ui/src/components/demarche/remove-etape-popup.stories.tsx b/packages/ui/src/components/demarche/remove-etape-popup.stories.tsx
index 42ca2b6abd8bef1f2b9e0dc553a07fbdb0f668f7..dcf4b1c4f4951f3f4d4baf62e3244a564942977b 100644
--- a/packages/ui/src/components/demarche/remove-etape-popup.stories.tsx
+++ b/packages/ui/src/components/demarche/remove-etape-popup.stories.tsx
@@ -2,6 +2,9 @@ import { action } from '@storybook/addon-actions'
 import { Meta, StoryFn } from '@storybook/vue3'
 import { RemoveEtapePopup } from './remove-etape-popup'
 import { etapeIdValidator } from 'camino-common/src/etape'
+import { DEMARCHES_TYPES_IDS } from 'camino-common/src/static/demarchesTypes'
+import { ETAPES_TYPES } from 'camino-common/src/static/etapesTypes'
+import { TITRES_TYPES_IDS } from 'camino-common/src/static/titresTypes'
 
 const meta: Meta = {
   title: 'Components/Demarche/RemoveEtapePopup',
@@ -15,9 +18,9 @@ const close = action('close')
 
 export const Default: StoryFn = () => (
   <RemoveEtapePopup
-    demarcheTypeId="oct"
-    etapeTypeId="mfr"
-    titreTypeId="arm"
+    demarcheTypeId={DEMARCHES_TYPES_IDS.Octroi}
+    etapeTypeId={ETAPES_TYPES.demande}
+    titreTypeId={TITRES_TYPES_IDS.AUTORISATION_DE_RECHERCHE_METAUX}
     id={etapeIdValidator.parse('etapeId')}
     apiClient={{
       deleteEtape(titreEtapeId) {
@@ -30,3 +33,24 @@ export const Default: StoryFn = () => (
     close={close}
   />
 )
+
+export const WithError: StoryFn = () => (
+  <RemoveEtapePopup
+    demarcheTypeId={DEMARCHES_TYPES_IDS.Octroi}
+    etapeTypeId={ETAPES_TYPES.demande}
+    titreTypeId={TITRES_TYPES_IDS.AUTORISATION_DE_RECHERCHE_METAUX}
+    id={etapeIdValidator.parse('etapeId')}
+    apiClient={{
+      deleteEtape(titreEtapeId) {
+        deleteEtape(titreEtapeId)
+
+        return Promise.resolve({
+          message: 'Ceci est une erreur',
+          detail: `Ceci est un détail d'erreur`,
+        })
+      },
+    }}
+    titreNom="Nouvelle espérance"
+    close={close}
+  />
+)
diff --git a/packages/ui/src/components/demarche/remove-etape-popup.stories_snapshots_WithError.html b/packages/ui/src/components/demarche/remove-etape-popup.stories_snapshots_WithError.html
new file mode 100644
index 0000000000000000000000000000000000000000..29e3fb1fc5abe3e7592539c1638bbd6c87d283af
--- /dev/null
+++ b/packages/ui/src/components/demarche/remove-etape-popup.stories_snapshots_WithError.html
@@ -0,0 +1,33 @@
+<!--teleport start-->
+<div>
+  <dialog id="monId" class="fr-modal fr-modal--opened" open="" aria-modal="true" role="dialog" aria-labelledby="monId-title" style="z-index: 1000001;">
+    <div class="fr-container fr-container--fluid fr-container-md">
+      <div class="fr-grid-row fr-grid-row--center">
+        <div class="fr-col-12 fr-col-md-8 fr-col-lg-6">
+          <div class="fr-modal__body">
+            <div class="fr-modal__header"><button class="fr-btn--close fr-btn" aria-controls="monId" title="Fermer">Fermer</button></div>
+            <div class="fr-modal__content">
+              <h1 id="monId-title" class="fr-modal__title"><span class="fr-icon-arrow-right-line fr-icon--lg" aria-hidden="true"></span>Suppression de l'étape</h1>
+              <div class="fr-container">
+                <div class="fr-alert fr-alert--warning">
+                  <p class="fr-alert__title fr-h4">Attention : cette opération est définitive et ne peut pas être annulée.</p>Souhaitez vous supprimer l'étape <span class="fr-text--bold">demande</span> de la démarche <span class="fr-text--bold">octroi</span> du titre <span class="fr-text--bold">Nouvelle espérance (autorisation de recherches)</span> ?
+                </div>
+                <!---->
+              </div>
+            </div>
+            <div class="fr-modal__footer">
+              <div style="display: flex; width: 100%; justify-content: end; align-items: center; gap: 1rem;">
+                <!---->
+                <ul class="fr-btns-group fr-btns-group--right fr-btns-group--inline-reverse fr-btns-group--inline-lg fr-btns-group--icon-left" style="width: auto;">
+                  <li><button class="fr-btn fr-btn--icon-left fr-icon-check-line fr-btn--primary fr-btn--md" title="Supprimer" aria-label="Supprimer" type="button">Supprimer</button></li>
+                  <li><button class="fr-btn fr-btn--icon-left fr-icon-arrow-go-back-fill fr-btn--secondary fr-btn--md" title="Annuler" aria-label="Annuler" aria-controls="monId" type="button">Annuler</button></li>
+                </ul>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </dialog>
+</div>
+<!--teleport end-->
\ No newline at end of file
diff --git a/packages/ui/src/components/demarche/remove-etape-popup.tsx b/packages/ui/src/components/demarche/remove-etape-popup.tsx
index 5164c5ed096f7f7c7e65f7e3bc38596b3f6eaa0a..37dca10327abb784e451d703bebd24e89d0af459 100644
--- a/packages/ui/src/components/demarche/remove-etape-popup.tsx
+++ b/packages/ui/src/components/demarche/remove-etape-popup.tsx
@@ -18,9 +18,7 @@ interface Props {
 }
 
 export const RemoveEtapePopup: FunctionalComponent<Props> = props => {
-  const deleteEtape = async () => {
-    await props.apiClient.deleteEtape(props.id)
-  }
+  const deleteEtape = () => props.apiClient.deleteEtape(props.id)
   const content = () => (
     <Alert
       type="warning"
diff --git a/packages/ui/src/components/etape-edition.tsx b/packages/ui/src/components/etape-edition.tsx
index 1888b4dedbbd827f0890ef12217f3e271c8bde9a..65b9cedd6027e7b8c2ac4ef8bc18d5127e575f18 100644
--- a/packages/ui/src/components/etape-edition.tsx
+++ b/packages/ui/src/components/etape-edition.tsx
@@ -91,10 +91,11 @@ export const PureEtapeEdition = defineComponent<Props>(props => {
   onMounted(async () => {
     try {
       if (isNotNullNorUndefined(props.etapeIdOrSlug)) {
-        const { etape, demarche } = await props.apiClient.getEtape(props.etapeIdOrSlug)
-        if ('message' in demarche) {
-          setAsyncData({ status: 'NEW_ERROR', error: demarche })
+        const result = await props.apiClient.getEtape(props.etapeIdOrSlug)
+        if ('message' in result) {
+          setAsyncData({ status: 'NEW_ERROR', error: result })
         } else {
+          const { etape, demarche } = result
           const perimetre = await props.apiClient.getPerimetreInfosByEtapeId(etape.id)
           if ('message' in perimetre) {
             setAsyncData({ status: 'NEW_ERROR', error: perimetre })
diff --git a/packages/ui/src/components/etape/etape-api-client.ts b/packages/ui/src/components/etape/etape-api-client.ts
index 37235b203a7cfb20f4c2bac3d5e67e85053705e3..7f4ab41a83838ddcb6311252d71a35c399c0ecf8 100644
--- a/packages/ui/src/components/etape/etape-api-client.ts
+++ b/packages/ui/src/components/etape/etape-api-client.ts
@@ -1,5 +1,5 @@
 import { apiGraphQLFetch } from '@/api/_client'
-import { deleteWithJson, getWithJson, newGetWithJson, newPostWithJson, newPutWithJson } from '@/api/client-rest'
+import { newDeleteWithJson, newGetWithJson, newPostWithJson, newPutWithJson } from '@/api/client-rest'
 import { CaminoDate, caminoDateValidator } from 'camino-common/src/date'
 import { DemarcheId } from 'camino-common/src/demarche'
 import { entrepriseIdValidator } from 'camino-common/src/entreprise'
@@ -80,12 +80,12 @@ export type GetEtapeHeritagePotentiel = z.infer<typeof heritageValidator>
 export type CoreEtapeCreationOrModification = Pick<Nullable<FlattenEtape>, 'id' | 'slug'> & DistributiveOmit<FlattenEtape, 'id' | 'slug'>
 export interface EtapeApiClient {
   getEtapesTypesEtapesStatuts: (titreDemarcheId: DemarcheId, titreEtapeId: EtapeId | null, date: CaminoDate) => Promise<EtapeTypeEtapeStatutWithMainStep | CaminoError<string>>
-  deleteEtape: (titreEtapeId: EtapeId) => Promise<void>
+  deleteEtape: (titreEtapeId: EtapeId) => Promise<void | CaminoError<string>>
   deposeEtape: (titreEtapeId: EtapeId) => Promise<{ id: EtapeId } | CaminoError<string>>
   getEtapeDocumentsByEtapeId: (etapeId: EtapeId) => Promise<GetEtapeDocumentsByEtapeId | CaminoError<string>>
   getEtapeHeritagePotentiel: (etape: Pick<CoreEtapeCreationOrModification, 'id' | 'date' | 'typeId'>, titreDemarcheId: DemarcheId) => Promise<GetEtapeHeritagePotentiel>
   getEtapeAvisByEtapeId: (etapeId: EtapeId) => Promise<GetEtapeAvisByEtapeId | CaminoError<string>>
-  getEtape: (etapeIdOrSlug: EtapeIdOrSlug) => Promise<{ etape: FlattenEtape; demarche: GetDemarcheByIdOrSlug | CaminoError<string> }>
+  getEtape: (etapeIdOrSlug: EtapeIdOrSlug) => Promise<{ etape: FlattenEtape; demarche: GetDemarcheByIdOrSlug } | CaminoError<string>>
   etapeCreer: (etape: RestEtapeCreation) => Promise<CaminoError<string> | { id: EtapeId }>
   etapeModifier: (etape: RestEtapeModification) => Promise<CaminoError<string> | { id: EtapeId }>
 }
@@ -93,17 +93,23 @@ export interface EtapeApiClient {
 export const etapeApiClient: EtapeApiClient = {
   getEtapesTypesEtapesStatuts: async (demarcheId, etapeId, date) => newGetWithJson('/rest/etapesTypes/:demarcheId/:date', { demarcheId, date }, etapeId ? { etapeId } : {}),
 
-  deleteEtape: async etapeIdOrSlug => {
-    await deleteWithJson('/rest/etapes/:etapeIdOrSlug', { etapeIdOrSlug })
-  },
-  deposeEtape: async etapeId => newPutWithJson('/rest/etapes/:etapeId/depot', { etapeId }, {}),
+  deleteEtape: etapeIdOrSlug => newDeleteWithJson('/rest/etapes/:etapeIdOrSlug', { etapeIdOrSlug }),
+  deposeEtape: etapeId => newPutWithJson('/rest/etapes/:etapeId/depot', { etapeId }, {}),
 
-  getEtapeDocumentsByEtapeId: async etapeId => newGetWithJson('/rest/etapes/:etapeId/etapeDocuments', { etapeId }),
-  getEtapeAvisByEtapeId: async etapeId => newGetWithJson('/rest/etapes/:etapeId/etapeAvis', { etapeId }),
+  getEtapeDocumentsByEtapeId: etapeId => newGetWithJson('/rest/etapes/:etapeId/etapeDocuments', { etapeId }),
+  getEtapeAvisByEtapeId: etapeId => newGetWithJson('/rest/etapes/:etapeId/etapeAvis', { etapeId }),
 
   getEtape: async etapeIdOrSlug => {
-    const etape = await getWithJson('/rest/etapes/:etapeIdOrSlug', { etapeIdOrSlug })
+    const etape = await newGetWithJson('/rest/etapes/:etapeIdOrSlug', { etapeIdOrSlug })
+    if ('message' in etape) {
+      return etape
+    }
+
     const demarche = await newGetWithJson('/rest/demarches/:demarcheIdOrSlug', { demarcheIdOrSlug: etape.titreDemarcheId })
+    if ('message' in demarche) {
+      return demarche
+    }
+
     return { etape, demarche }
   },
   getEtapeHeritagePotentiel: async (etape, titreDemarcheId) => {
@@ -191,6 +197,6 @@ export const etapeApiClient: EtapeApiClient = {
     return heritageData
   },
 
-  etapeCreer: async etape => newPostWithJson('/rest/etapes', {}, restEtapeCreationValidator.parse(etape)),
-  etapeModifier: async etape => newPutWithJson('/rest/etapes', {}, restEtapeModificationValidator.parse(etape)),
+  etapeCreer: etape => newPostWithJson('/rest/etapes', {}, restEtapeCreationValidator.parse(etape)),
+  etapeModifier: etape => newPutWithJson('/rest/etapes', {}, restEtapeModificationValidator.parse(etape)),
 }
diff --git a/packages/ui/src/components/titre.tsx b/packages/ui/src/components/titre.tsx
index b625a4bc8a9b5e336ee41c3f824fd24c05e43fdf..c298cee797c5292a28d3c1b6eb38d3e46a6f6a78 100644
--- a/packages/ui/src/components/titre.tsx
+++ b/packages/ui/src/components/titre.tsx
@@ -237,7 +237,11 @@ export const PureTitre = defineComponent<Props>(props => {
     ...props.apiClient,
     deleteEtape: async titreEtapeId => {
       if (titreData.value.status === 'LOADED') {
-        await props.apiClient.deleteEtape(titreEtapeId)
+        const result = await props.apiClient.deleteEtape(titreEtapeId)
+        if (typeof result === 'object' && 'message' in result) {
+          return result
+        }
+
         await retrieveTitre()
       }
     },