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

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. 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) that exports a default function returning an AstroIntegration object. This factory pattern allows consumers to pass configuration options and return the integration instance.

// 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:

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.

// 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 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:

// 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.
  • 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, 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.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →