Dedicated SSR (Server-side-rendered) Astro native Headless CMS, build from the ground up for the Astro community and by Astro community memb github.com/withstudiocms/studiocms
README.md

effectify #

A utility library that bridges Effect-ts with various utilities and projects, such as Astro!

Features #

  • Effect-ts Integration - Convert Effect HTTP APIs to Astro API routes
  • Context Management - Seamless Astro context integration with Effect contexts
  • Web Handler Conversion - Built-in conversion utilities for Effect HTTP handlers
  • Scalar API Documentation - Integrated API documentation generation

Installation #

pnpm add effectify

Usage #

Undocumented Modules:

  • effectify/scalar - Custom version of the Scalar provided by Effect's HttpApi
  • effectify/scrypt - Node Scrypt Effect wrapper for password hashing

effectify/astro/* Modules #

Utilities specific for working with Effect and Astro

effectify/astro/context #

This module provides an AstroAPIContext export that can be used when working with Effect HTTP web handlers within an Astro environment.

Example #

The example below shows how to access Astro locals from within en HttpApi handler

import { AstroAPIContext } from 'effectify/astro/context';

// Example of defining an Effect API and converting it to an Astro API route handler
const api = HttpApi.make('myApi').add(
	HttpApiGroup.make('group').add(HttpApiEndpoint.get('get', '/').addSuccess(Schema.String))
);

// Build the Effect API Handler (with Astro context)
const groupLive = HttpApiBuilder.group(api, 'group', (handlers) =>
	handlers.handle('get', () =>
		Effect.gen(function* () {
			const { locals } = yield* AstroAPIContext;
			console.log('Astro Locals:', locals); // Log the locals to verify access
			return 'Hello from Effect API!';
		})
	)
);

effectify/astro/HttApi #

This module provides a Utility to wrap your Effect-based HttpApi into a Astro APIRoute

Example #
// src/pages/myApi.ts
import { HttpApiToAstroRoute } from 'effectify/astro/HttApi';

// Example of defining an Effect API and converting it to an Astro API route handler
const api = HttpApi.make('myApi').add(
	HttpApiGroup.make('group').add(HttpApiEndpoint.get('get', '/').addSuccess(Schema.String))
);

// Build the Effect API Handler (with Astro context)
const groupLive = HttpApiBuilder.group(api, 'group', (handlers) =>
	handlers.handle('get', () =>
		Effect.gen(function* () {
			const { locals } = yield* AstroAPIContext;
			console.log('Astro Locals:', locals); // Log the locals to verify access
			return 'Hello from Effect API!';
		})
	)
);

// Create the Live Layer for the API
const MyApiLive = HttpApiBuilder.api(api).pipe(Layer.provide(groupLive));

// Convert the Effect API to an Astro API route handler
const ALL = HttpApiToAstroRoute(MyApiLive);

effectify/astro/integration #

This module provides a defineIntegration utility for building Astro integrations with Effect-ts. The helper adds schema-validation for integration options and automatically wraps integration hooks to surface errors as EffectifyIntegrationHookError instances.

Features #
  • Schema Validation - Validate integration options using Effect Schema at initialization time
  • Automatic Error Handling - All Effect-based hooks are wrapped to catch and re-throw errors with proper context
  • Type Safety - Full TypeScript support with inferred types from schemas
  • Effect Integration - Native Effect-ts patterns for hooks
Example #
import { Effect, Schema } from 'effect';
import { defineIntegration, EffectifyIntegrationHookError } from 'effectify/astro/integration';

// Define an Astro integration with schema-validated options
export const myIntegration = defineIntegration({
	name: 'my-integration',
	schema: Schema.Struct({
		apiKey: Schema.String,
		enabled: Schema.optionalWith(Schema.Boolean, {
			default: () => true,
		}),
	}),
	setup: ({ name, options }) => ({
		'astro:config:setup': ({ logger }) =>
			Effect.gen(function* () {
				if (options.enabled) {
					logger.info(`${name} is enabled`);
				}
			}),
		'astro:config:done': ({ logger }) =>
			Effect.gen(function* () {
				logger.info(`Configuration complete for ${name}`);
			}),
	}),
});

// Use in astro.config.mjs
export default defineConfig({
	integrations: [
		myIntegration({
			apiKey: 'my-secret-key',
			enabled: true,
		}),
	],
});
Error Handling #

The defineIntegration utility automatically wraps all hooks to catch errors. You can also explicitly handle errors within your hooks:

export const myIntegration = defineIntegration({
	name: 'my-integration',
	setup: ({ name, options }) => ({
		'astro:config:setup': ({ logger }) =>
			Effect.gen(function* () {
				// Your integration logic here
			}).pipe(
				Effect.catchAll((error) =>
					Effect.fail(
						new EffectifyIntegrationHookError({
							hook: 'astro:config:setup',
							message: 'Failed to setup integration',
							cause: error,
						})
					)
				)
			),
	}),
});

effectify/schemas #

FunctionSchema / SyncFunctionSchema #

These functions provide the ability to define custom functions within Effect Schema for both async and sync functions.

Example (FunctionSchema) #

Creates a schema for functions with validated inputs and outputs. Similar to Zod's z.function().

The first param, is the args input, the second is the return. FunctionSchema will always return a async function. (promise-based)

import { FunctionSchema } from 'effectify/schemas';
import { Schema } from 'effect';

// Define your Schema
const LoginSchema = FunctionSchema(
	Schema.Struct({ username: Schema.String, password: Schema.String }),
	Schema.Boolean
);

// Example implementation function
const rawLoginFn = async (data: { username: string; password: string }) => {
	return data.username === 'admin' && data.password === '123';
};

// Create decoder
const decoder = Schema.decodeSync(LoginSchema)(rawLoginFn);

// Verify using implementation function internally
const result = await decoder({ username: 'admin', password: '123' });

console.log(result)
/* Console Output:
true
*/
Example (SyncFunctionSchema) #

Similar to FunctionSchema but will always return a synchronous function.

import { SyncFunctionSchema } from 'effectify/schemas';
import { Schema } from 'effect';

// Define your Schema
const LoginSchema = SyncFunctionSchema(
	Schema.Struct({ username: Schema.String, password: Schema.String }),
	Schema.Boolean
);

// Example implementation function
const rawLoginFn = (data: { username: string; password: string }) => {
	return data.username === 'admin' && data.password === '123';
};

// Create decoder
const decoder = Schema.decodeSync(LoginSchema)(rawLoginFn);

// Verify using implementation function internally
const result = decoder({ username: 'admin', password: '123' });

console.log(result)
/* Console Output:
true
*/

effectify/static #

makeStaticFileHttpApiRouter #

Creates an HTTP API router that serves static files from a specified directory. The router can be configured to serve an index.html file for the root path and to set appropriate caching headers for files with hashed filenames.

// /src/index.ts
import { makeStaticFileHttpApiRouter } from 'effectify/static';

const TestStatic = makeStaticFileHttpApiRouter({
	htmlIndex: true, // (optional) Allows for serving index.html
	pathPrefix: '/assets' // (optional) Allows setting custom base path for served assets (leave blank for base route)
})(import.meta.dirname, 'static'); // Directory would be /src/static/

// If the source directory was /src/index.ts and the assets was located at /assets/
// then you would use (import.meta.dirname, '..', 'assets')

makeStaticFileMiddleware #

The router above is simply a wrapper around this middleware, and has the same settings.

import { makeStaticFileMiddleware } from 'effectify/static';

const StaticMiddleware = makeStaticFileMiddleware()(import.meta.dirname, 'static');

effectify/webHandler #

webHandlerToEffectHttpHandler #

Converts a web handler function to an Effect HttpServerRequest handler.

import { webHandlerToEffectHttpHandler } from 'effectify/webHandler';

const mockWebHandler = async (request: Request) => {
	console.log('Received request:', request);
	return new Response('Hello from mock web handler!', { status: 200 });
};

const EffectHttpHandler = webHandlerToEffectHttpHandler(mockWebHandler);

EffectHttpHandlerToHttpApi #

Utility function to convert a Effect-based web handler into a format that can be used with the HttpApiBuilder from Effect. This allows you to define your web handlers using Effect and then easily integrate them into an HTTP API.

// src/pages/[...api].ts
import { webHandlerToEffectHttpHandler, EffectHttpHandlerToHttpApi } from 'effectify/webHandler';

const mockWebHandler = async (request: Request) => {
	console.log('Received request:', request);
	return new Response('Hello from mock web handler!', { status: 200 });
};

const EffectHttpHandler = webHandlerToEffectHttpHandler(mockWebHandler);

const EffectHttpApiHandler = EffectHttpHandlerToHttpApi('*', EffectHttpHandler);

// Using a previous example to demonstrate possible usage
const ALL = HttpApiToAstroRoute(EffectHttpApiHandler);

effectify/zod #

Converts Zod schemas to Effect schemas, enabling seamless migration from Zod to Effect's type-safe schema system. This function supports most common Zod schema types including primitives, objects, arrays, unions, intersections, and more.

Features #
  • Comprehensive Type Support - Handles primitives (string, number, boolean, etc.), complex types (objects, arrays, tuples), and special types (dates, symbols, bigints)
  • File Schema Support - Includes FileFromSelf schema for handling JavaScript File objects
  • String Representation - Provides zodToEffect.writeable() for debugging and visualization of the generated Effect schema structure
Basic Example #
import { zodToEffect } from 'effectify/zod';
import { z } from 'zod';

// Define a Zod schema
const userSchema = z.object({
  username: z.string(),
  age: z.number().int(),
  email: z.string().optional(),
});

// Convert to Effect schema
const effectUserSchema = zodToEffect(userSchema);

// Use the Effect schema for validation
const result = Schema.decodeUnknownSync(effectUserSchema)({
  username: 'johndoe',
  age: 25,
  email: 'john@example.com',
});
Complex Example #
import { zodToEffect } from 'effectify/zod';
import { z } from 'zod';

// Complex nested schema with unions and intersections
const complexSchema = z.union([
  z.object({
    type: z.literal('user'),
    data: z.tuple([
      z.string(),
      z.object({
        id: z.int().optional(),
        tags: z.array(z.string()),
      }),
    ]),
  }),
  z.intersection(
    z.object({
      type: z.literal('admin'),
      permissions: z.record(z.string(), z.array(z.string())),
    }),
    z.object({
      createdAt: z.date(),
    })
  ),
]);

// Convert to Effect schema
const effectSchema = zodToEffect(complexSchema);

// For debugging, visualize the schema structure as a string
const schemaString = zodToEffect.writeable(complexSchema);
console.log(schemaString);
Supported Schema Types #
  • Primitives: string, number, int, bigint, boolean, date, symbol
  • Special values: null, undefined, void, any, unknown, never, nan
  • Collections: array, set, map, record, tuple
  • Composition: union, intersection, object (struct)
  • Modifiers: optional, nullable, readonly, nonoptional
  • Other: literal, enum, template_literal, file

Note: Some Zod schema types like lazy, transform, pipe, promise, and custom are not currently supported and will throw an error.

Contributing #

Contributions are welcome! Please follow the StudioCMS contribution guidelines.

License #

MIT License