Home
Articles Contact
All articles
Astro OpenGraph design.

Dependency injection in Astro

Dependency injection is a powerful programming technique. Learn how to apply a simplified version to Astro.

Dependency injection (DI) is a powerful technique: an object’s dependencies (like services or components it needs) are provided from the outside rather than the object creating them itself. This promotes loose coupling, making code more modular, testable, and easier to maintain.

Let’s see how to apply it to Astro, in a simple way.

In Astro

Astro doesn’t have a built-in DI system, unlike NestJS or AdonisJS . But it’s very easy to implement in userland using locals .

Locals hold data for the current request, so it can be used as a main bus.

Example usage

This is the approach I like to take on my projects but this is not the only way. Adapt it to your needs and specific use cases.

For our example, we’ll implement a getUsersByActivity() function that retrieves users based on their active status and returns them.

1. Define your dependencies

Let’s start by defining our user model:

// src/lib/models.ts
export interface User {
  id: string;
  name: string;
  active: boolean;
}

Then define our dependencies:

// src/lib/dependencies.ts
import type { User } from "./models";

export interface Database {
  sql: (raw: string) => Promise<Array<any>>;
}

export interface UsersRepository {
  all: () => Promise<Array<User>>;
}

export interface Dependencies {
  db: Database;
  usersRepository: UsersRepository;
}

2. Implement them

Let’s start with the Database. We’ll use pg to implement it. For the sake of simplicity, this example does not cover sanitizing the query nor stopping the connection:

// src/lib/implementations/database.ts
import type { Database } from "../dependencies";
import { Client } from "pg";

export async function createPgDatabase(): Database {
  const client = new Client();
  await client.connect();

  return {
    sql: async (raw) => {
      const res = await client.query(raw);
      return res.rows;
    },
  };
}

Then we’ll implement the UsersRepository:

// src/lib/implementations/users-repository.ts
import type { Dependencies, UsersRepository } from "../dependencies";

export function createDatabaseUsersRepository({
  db,
}: Pick<Dependencies, "db">): UsersRepository {
  return {
    all: async () => {
      return await db.sql("select * from users");
    },
  };
}

Notice how this implementation of the UsersRepository itself has a dependency.

3. Write your function

Now that our dependencies are ready, let’s use them:

// src/lib/usecases/get-users-by-activity.ts
import type { Dependencies } from "../dependencies";
import type { User } from "../models";

export async function getUsersByActivity(
  { usersRepository }: Pick<Dependencies, "usersRepository">,
  { active }: { active: boolean }
): Promise<Array<User>> {
  const users = await usersRepository.all();
  return users.filter((user) => user.active === active);
}

The key point here is that the first argument is for dependencies, and any other argument is for actual parameters.

4. Type your locals

Make sure to use inline imports to avoid breaking global augmentation:

// src/env.d.ts
declare namespace App {
  type Dependencies = import("./lib/dependencies").Dependencies;
  interface Locals extends Dependencies {}
}

5. Initialize your dependencies

Let’s write a middleware to initialize everything:

// src/middleware.ts
import { defineMiddleware, sequence } from "astro/middleware";
import type { Dependencies } from "./lib/dependencies";
import { createPgDatabase } from "./lib/implementations/database";
import { createDatabaseUsersRepository } from "./lib/implementations/users-repository";

const dependenciesMiddleware = defineMiddleware(async (context, next) => {
  const db = await createPgDatabase();
  const usersRepository = createDatabaseUsersRepository({ db });

  Object.assign(context.locals, {
    db,
    usersRepository,
  } satisfies Dependencies);

  return await next();
});

export const onRequest = sequence(dependenciesMiddleware);

6. Use your function

You can now use getUsersByActivity() anywhere within request scope. For example in an Astro page:

---
// src/pages/index.astro
import { getUsersByActivity } from '../lib/usecases/get-users-by-activity';

const activeUsers = await getUsersByActivity(Astro.locals, { active: true });
---

<ul>
    {activeUsers.map((user) => <li>{user.name}</li>)}
</ul>

Dependency injection usually involves more than that for more complex use cases, but this article is only meant to be used as a starting point!


Big thanks to Lloyd Atkinson for the feedback!

All articles