npm package: @gnosticdev/hono-actions
Description: Define server actions with built-in validation, error handling, and a pre-built hono client for calling the routes.
Author: gnosticdev
Latest version: 2.1.1

# Astro Actions with Hono

Define server actions with built-in validation, error handling, and a pre-built hono client for calling the routes.

## Installation

```bash
# with astro cli
astro add @gnosticdev/hono-actions

# package manager (npm, pnpm, bun, etc.)
bun add @gnosticdev/hono-actions
```

## Requirements

This package requires:

- `astro`: ^5.13.0 or ^6.0.0

> [!IMPORTANT]
> For typescript, make sure you set `strictNullChecks` to `true`, or extend `astro/tsconfigs/strict(est)` in your `tsconfig.json`

## Supported Adapters

This integration works with all supported Astro adapters:

- `@astrojs/cloudflare`
- `@astrojs/node` (standalone mode only)
- `@astrojs/vercel`
- `@astrojs/netlify`

## Setup

### 1. Add the integration to your Astro config

The integration works with all Astro adapters. Here are examples for each:

#### Cloudflare/Vercel/Netlify Adapter

```typescript
// astro.config.ts
import { defineConfig } from 'astro/config'
import cloudflare from '@astrojs/cloudflare'
import honoActions from '@gnosticdev/hono-actions'

export default defineConfig({
  output: 'server',
  adapter: cloudflare() // vercel() or netlify(),
  integrations: [
    honoActions({
      basePath: '/api', // Optional: default is '/api'
      actionsPath: 'src/server/actions.ts' // Optional: custom path to your actions file
    })
  ]
})
```

#### Node.js Adapter

Node.js adapter only supports the 'standalone' mode.

```typescript
// astro.config.ts
import { defineConfig } from 'astro/config'
import node from '@astrojs/node'
import honoActions from '@gnosticdev/hono-actions'

export default defineConfig({
  output: 'server',
  adapter: node({
    mode: 'standalone'
  }),
  integrations: [
    honoActions()
  ]
})
```

### 2. Create your actions file

Create a file at one of these locations, or use the `actionsPath` option to specify a custom path:

- `src/server/actions.ts`
- `src/hono/actions.ts`
- `src/hono/index.ts`
- `src/hono.ts`
- `src/hono-actions.ts`

## Usage

```typescript
// src/hono.ts (or any of the supported locations above)
import { defineHonoAction type HonoEnv } from '@gnosticdev/hono-actions/actions'
import { z } from 'astro/zod'
import { Hono } from 'hono'

export const myAction = defineHonoAction({
  schema: z.object({
    name: z.string()
  }),
  handler: async (input, ctx) => {
    // `input` is automatically typed from the schema
    // `ctx` is a strongly-typed Hono Context with your `HonoEnv`
    return { message: `Hello ${input.name}!` }
  }
})

// Define another POST action
export const anotherAction = defineHonoAction({
  schema: z.object({ name2: z.string() }),
  handler: async (input, ctx) => {
    return {
      message2: `Hello ${input.name2}!`
    }
  }
})

// Optional: Define an action without a schema (accepts any JSON)
export const noSchemaAction = defineHonoAction({
  handler: async (input, ctx) => {
    if (!('name' in input)) {
      throw new HonoActionError({
        message: 'Name is required',
        code: 'INPUT_VALIDATION_ERROR'
      })
    }
    return { message: `Hello ${String((input as any).name)}!` }
  }
})

// You can also define standard Hono routes (GET/PATCH/etc.), not just POST actions.
// This is useful where standard Astro actions are POST-only.
const app = new Hono<HonoEnv>()
const getRoute = app.get('/', (c) => c.json({ message: 'Hi from a get route' }))

// Export all actions and routes in a single `honoActions` object.
// Each key becomes the route name under your basePath, e.g.:
//  - POST /api/myAction
//  - POST /api/anotherAction
//  - GET  /api/getRoute
export const honoActions = {
  myAction,
  anotherAction,
  noSchemaAction,
  getRoute
}
```

### 3. Use actions in your Astro components or pages

```typescript
// src/pages/example.astro or any .astro file
---
import { honoClient, parseResponse } from '@gnosticdev/hono-actions/client'

// Call a POST action
const { data: actionRes } = await parseResponse(
  await honoClient.api.myAction.$post({ json: { name: 'John' } })
)

// Call a GET route
const { message } = await parseResponse(
  await honoClient.api.getRoute.$get()
)
---

<div>
  {actionRes && <p>{actionRes.message}</p>}
  <p>{message}</p>
</div>
```

### 4. Use in client-side JavaScript

```typescript
// In a client-side script or component
import { honoClient } from '@gnosticdev/hono-actions/client'
import type { DetailedError } from '@gnosticdev/hono-actions/client'

// Make requests from the browser
const handleSubmit = async (formData: FormData) => {
  const { data, error } = await parseResponse(await honoClient.api.anotherAction.$post({
    json: {
      name2: formData.get('name') as string
    }
  }).catch((err: DetailedError) => {
    console.error('Error:', err)
    return {
      data: null,
      error: err
    }
  })

  if (error){
    throw new Error(`Action failed: ${error.message}`)
  }

// type safe access to the data
  console.log('my name is ', data.name2)
}
```

## Augmenting Type Interfaces

When using this library, you may need to augment the type interfaces to add custom types for your environment bindings, Hono context variables, or Astro locals. This is especially important when using the Cloudflare adapter.

### Cloudflare Runtime Types

When using the `@astrojs/cloudflare` adapter, the library automatically exports Cloudflare runtime types. However, it assumes that Cloudflare types have been generated by Wrangler via the `wrangler types` command, which creates an `Env` interface that is automatically added to `HonoEnv['Bindings']`.

For Astro 6 and newer, use the matching Astro 6 adapter majors (`@astrojs/cloudflare` v13+, `@astrojs/node` v10+, `@astrojs/vercel` v10+, `@astrojs/netlify` v7+).

To generate Cloudflare types:

```bash
bunx --bun wrangler types
```

This creates a `worker-configuration.d.ts` file (or similar) containing the `Env` interface based on your `wrangler.jsonc` configuration.

### Extending HonoEnv Types

To add additional types to your Hono environment, create a type declaration file (e.g., `src/env.d.ts`) and augment the module:

```typescript
// src/env.d.ts
import '@gnosticdev/hono-actions/actions'

// Augmenting the actions types for use with cloudflare adapter
declare module '@gnosticdev/hono-actions/actions' {
    // 1) Extend existing Bindings, with Env from worker-configuration.d.ts
    interface Bindings extends Env {
        anotherVar: string
    }

    // 2) Add Variables available on `ctx.var.db` or `ctx.var.db.get('randomKey')` in hono actions
    interface HonoEnv {
        Variables: {
            db: Map<string, any>
        }
        Bindings: Bindings
    }
}

// Extend Astro Locals if you want to use in middleware
// need to add this to global scope bc we have an import in the file
declare global {
    type Runtime = import('@astrojs/cloudflare').Runtime<Env>
    declare namespace App {
        interface Locals extends Runtime {
            db: Map<string, any> // this will now be available on both `ctx.var.db` and `Astro.locals.db`
        }
    }
}


```

Now in your actions, you'll have full type safety:

```typescript
// src/hono.ts
import { defineHonoAction, type HonoEnv } from '@gnosticdev/hono-actions/actions'

export const myAction = defineHonoAction({
  handler: async (input, ctx) => {
    // ctx.env has type: Bindings (includes Env + your custom bindings)
    const kv = ctx.env.CUSTOM_KV

    // ctx.var has type: HonoEnv['Variables']
    const user = kv.get('user')

    return { success: true }
  }
})
```

And in your Astro pages:

```typescript
// src/pages/index.astro
---
// Astro.locals has type: App.Locals
const user = Astro.locals.user
---
```

## Package Structure

This package provides these entry points:

- **`@gnosticdev/hono-actions/actions`**: Action definition utilities (`defineHonoAction`, `HonoActionError`, `HonoEnv` (for cloudflare usage))
  - Used in your actions file(s)
- **`@gnosticdev/hono-actions/client`**: Pre-built Hono client and helpers (`honoClient`, `parseResponse`)
  - Safe for browser and server environments
- **`@gnosticdev/hono-actions`**: Astro integration
  - Uses Node.js built-ins (fs, path)
  - Only used in `astro.config.ts`

## Configuration Options

The integration accepts the following options:

- **`basePath`** (optional): The base path for your API routes. Default: `'/api'`
- **`actionsPath`** (optional): Custom path to your actions file if not using auto-discovery

## Features

- ✅ **Type-safe**: Full TypeScript support with automatic type inference
- ✅ **Validation**: Built-in request validation using Zod schemas
- ✅ **Error handling**: Custom error types and automatic error responses
- ✅ **Auto-discovery**: Automatically finds your actions file
- ✅ **Client generation**: Pre-built client with full type safety
- ✅ **Development**: Hot reload support during development
- ✅ **Flexible routing**: Define standard Hono routes (GET/PATCH/etc.) alongside POST actions

## Troubleshooting

### Actions not found

If you get an error that no actions were found, make sure:

1. Your actions file is in one of the supported locations
2. You export a `honoActions` object containing your actions and any Hono routes
3. The file path matches the `actionsPath` option if you specified one

### Type errors

If you're getting TypeScript errors:

1. Make sure all peer dependencies are installed
2. Run `astro sync` to regenerate types
3. Restart your TypeScript server in your editor

### Module resolution errors

If you get module resolution errors during development:

1. Try clearing your node_modules and reinstalling
2. Make sure you're using compatible versions of the peer dependencies

## Advanced Usage

### Using AsyncLocalStorage with Custom Hono Instances

For advanced use cases involving request-scoped data and custom Hono instances, see [docs/async_hooks.md](./docs/async_hooks.md).

## License

MIT