# How to Create and Register a Custom Astro Integration for Extending Functionality

> Learn how to create and register a custom Astro integration. Export a factory function with name and hooks and import it in your astro.config.mjs to extend functionality.

- Repository: [Astro/astro](https://github.com/withastro/astro)
- Tags: how-to-guide
- Published: 2026-03-06

---

**To create and register a custom Astro integration, export a factory function that returns an object implementing the `AstroIntegration` interface—containing a `name` string and a `hooks` map—then import and invoke that function within the `integrations` array of your `astro.config.mjs` file.**

Astro's extensibility is built around a robust plugin system defined in the `withastro/astro` repository. By learning how to **create and register a custom Astro integration**, you can inject renderers, add middleware, manipulate the build pipeline, and extend the framework's core behavior at specific lifecycle points.

## Understanding the AstroIntegration Interface

The contract for all integrations is defined in **[`packages/astro/src/types/public/integrations.ts`](https://github.com/withastro/astro/blob/main/packages/astro/src/types/public/integrations.ts)**. According to the Astro source code, an integration is a plain JavaScript or TypeScript module that exports a factory function returning an object with two required properties: `name` (a string identifier) and `hooks` (a map of lifecycle callbacks).

The `hooks` object implements the `BaseIntegrationHooks` interface, which provides type signatures for every extension point—from configuration setup to build completion. Key helper types defined alongside this interface include `AstroRenderer` for adding view layers and `AstroIntegrationMiddleware` for request handling.

## Step-by-Step: Creating a Custom Integration

### Step 1: Define the Integration Factory

Create a new file in your project (e.g., [`src/integrations/custom-integration.ts`](https://github.com/withastro/astro/blob/main/src/integrations/custom-integration.ts)) that exports a default function returning an `AstroIntegration` object. This factory pattern allows consumers to pass configuration options and return the integration instance.

```ts
// src/integrations/custom-integration.ts
import type { AstroIntegration, AstroRenderer, AstroIntegrationMiddleware } from 'astro';

```

### Step 2: Implement Lifecycle Hooks

Populate the `hooks` map with the specific lifecycle events you need to intercept. The most commonly used hook is `'astro:config:setup'`, which runs during configuration loading and provides utilities like `addRenderer`, `addMiddleware`, and `injectTypes`.

For example, to register a custom renderer similar to the test fixture in **`packages/astro/test/fixtures/custom-renderer`**, you implement the renderer definition and pass it to `addRenderer`:

```ts
const myRenderer = (): AstroRenderer => ({
  name: 'my-renderer',
  clientEntrypoint: '@my/renderer/client',
  serverEntrypoint: '@my/renderer/server',
});

const requestLogger: AstroIntegrationMiddleware = {
  order: 'pre',
  entrypoint: new URL('./middleware/logger.ts', import.meta.url).href,
};

export default function customIntegration(): AstroIntegration {
  return {
    name: 'custom-integration',
    hooks: {
      'astro:config:setup': async ({ addRenderer, addMiddleware }) => {
        addRenderer(myRenderer());
        addMiddleware(requestLogger);
      },
      'astro:server:start': async ({ address, logger }) => {
        logger.info(`🚀 Dev server listening on http://${address.address}:${address.port}`);
      },
      'astro:config:done': async ({ injectTypes, logger }) => {
        injectTypes({
          filename: 'my-types.d.ts',
          content: `declare module '@my/renderer/client';`,
        });
        logger.info('✅ Custom integration types injected');
      },
    },
  };
}

```

### Step 3: Register in astro.config.mjs

Import your integration factory and invoke it inside the `integrations` array exported by your configuration file. This registration step connects your custom code to Astro's build and development lifecycles.

```js
// astro.config.mjs
import { defineConfig } from 'astro/config';
import customIntegration from './src/integrations/custom-integration.js';

export default defineConfig({
  integrations: [customIntegration()],
});

```

## Core Lifecycle Hooks and Integration Helpers

As implemented in `withastro/astro`, the hook system covers distinct phases of the application lifecycle. Each hook receives a context object containing specific helpers:

- **`astro:config:setup`**: Runs during config loading. Use **`addRenderer`** to register view layers, **`addMiddleware`** to inject request handlers, **`injectScript`** to add global client-side code, and **`injectTypes`** to generate TypeScript definitions.
- **`astro:config:done`**: Fires after configuration resolution. Use **`injectTypes`** for final type definitions or validate the resolved configuration.
- **`astro:server:start`**: Executes when the dev server begins listening. Access the server `address` and a scoped **`logger`** instance to output startup information.
- **`astro:server:setup`**: Runs before Vite's dev server initializes. Ideal for registering custom Vite plugins or dev tooling.
- **`astro:build:start`** and **`astro:build:done`**: Bookend the static generation process. Manipulate build output, generate additional files, or implement custom prerendering logic.
- **`astro:route:setup`**: Called for each route as it is added. Modify route metadata or configure redirects.
- **`astro:routes:resolved`**: Fires after all routes are collected. Validate or transform the final route list before rendering begins.

The **[`packages/astro/src/integrations/hooks.ts`](https://github.com/withastro/astro/blob/main/packages/astro/src/integrations/hooks.ts)** file provides the internal machinery that creates logger instances scoped to each integration name, ensuring clean output prefixes during CLI execution.

## Example: Custom Middleware Implementation

When adding middleware via `addMiddleware`, you must provide an `entrypoint` URL and optionally an `order` ('pre' or 'post'). The middleware module itself follows the standard Astro middleware signature:

```ts
// src/integrations/middleware/logger.ts
import type { IncomingMessage, ServerResponse } from 'http';

export default async function logger(
  request: IncomingMessage,
  response: ServerResponse,
  next: () => Promise<void>
) {
  console.log(`[${new Date().toISOString()}] ${request.method} ${request.url}`);
  await next();
}

```

This middleware executes before every request when registered with `order: 'pre'` in the `astro:config:setup` hook.

## Summary

- **Create and register a custom Astro integration** by exporting a factory function that returns an `AstroIntegration` object with `name` and `hooks` properties, as defined in **[`packages/astro/src/types/public/integrations.ts`](https://github.com/withastro/astro/blob/main/packages/astro/src/types/public/integrations.ts)**.
- Implement hooks like **`astro:config:setup`** to access helpers including **`addRenderer`**, **`addMiddleware`**, and **`injectTypes`**.
- Register the integration in **`astro.config.mjs`** by importing the factory and including its return value in the `integrations` array.
- Use the test fixture in **`packages/astro/test/fixtures/custom-renderer`** as a reference implementation for custom renderers.
- Leverage the scoped **`logger`** instance provided in hook contexts to output integration-specific diagnostic information.

## Frequently Asked Questions

### What is the minimum required structure for an AstroIntegration object?

According to the Astro source code in [`packages/astro/src/types/public/integrations.ts`](https://github.com/withastro/astro/blob/main/packages/astro/src/types/public/integrations.ts), an `AstroIntegration` object must contain exactly two properties: a `name` string that uniquely identifies the integration, and a `hooks` object that maps lifecycle event names to callback functions. The `hooks` object can be empty, but the integration will not modify the build process without at least one hook implementation.

### How do I add a custom renderer to handle a new UI framework?

Define an object satisfying the `AstroRenderer` interface with `name`, `clientEntrypoint`, and `serverEntrypoint` properties. Inside the `astro:config:setup` hook, call the `addRenderer` helper and pass your renderer object. The `clientEntrypoint` is bundled for browser execution, while the `serverEntrypoint` handles server-side rendering during the build, as demonstrated in the custom-renderer test fixture.

### Can an integration modify the Vite configuration directly?

Yes. During the `astro:config:setup` hook, you can access the `updateConfig` helper to merge custom Vite configuration options into Astro's internal Vite setup. This allows integrations to register additional plugins, define aliases, or modify build behavior at the bundler level.

### Where should I place my custom integration files in a project?

While Astro imposes no strict directory requirement, convention places custom integrations in a `src/integrations/` or `integrations/` directory at the project root. The entry point must be importable by your `astro.config.mjs` file, so ensure the file path in your import statement resolves correctly relative to the config file location.