Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/rjdellecese/confect/llms.txt

Use this file to discover all available pages before exploring further.

Confect integrates Effect’s HTTP API with Convex, allowing you to build RESTful endpoints with automatic OpenAPI documentation powered by Scalar.

Overview

Effect HTTP

Build APIs using Effect’s type-safe HTTP module

OpenAPI Docs

Auto-generated documentation with Scalar UI

Middleware

Add authentication, logging, and CORS

Type Safety

End-to-end type safety from request to response

Creating an HTTP API

Define your HTTP API using Effect’s HttpApiBuilder:
confect/http.ts
import { HttpApiBuilder, HttpApiSchema } from "@effect/platform";
import { Schema } from "effect";

class UsersApi extends HttpApiSchema.make("users") {
  static readonly GetUser = HttpApiSchema.get(
    "getUser",
    "/users/:id"
  ).pipe(
    HttpApiSchema.setPath(
      Schema.Struct({
        id: Schema.String,
      })
    ),
    HttpApiSchema.setSuccess(
      Schema.Struct({
        id: Schema.String,
        name: Schema.String,
        email: Schema.String,
      })
    )
  );
  
  static readonly CreateUser = HttpApiSchema.post(
    "createUser",
    "/users"
  ).pipe(
    HttpApiSchema.setPayload(
      Schema.Struct({
        name: Schema.String,
        email: Schema.String,
      })
    ),
    HttpApiSchema.setSuccess(
      Schema.Struct({
        id: Schema.String,
      })
    )
  );
}

const api = HttpApiBuilder.api().pipe(
  HttpApiBuilder.addGroup(UsersApi)
);

Implementing Handlers

Implement your HTTP endpoints using Effect:
confect/http.ts
import { HttpApiBuilder } from "@effect/platform";
import { DatabaseReader, DatabaseWriter } from "@confect/server";
import { Effect, Layer } from "effect";

const UsersApiLive = HttpApiBuilder.group(
  UsersApi,
  "users",
  (handlers) =>
    Effect.gen(function* () {
      const db = yield* DatabaseReader<typeof databaseSchema>();
      const dbWrite = yield* DatabaseWriter<typeof databaseSchema>();
      
      return handlers
        .handle("getUser", ({ path }) =>
          Effect.gen(function* () {
            const user = yield* db
              .table("users")
              .get(path.id as Id<"users">);
            
            return {
              id: user._id,
              name: user.name,
              email: user.email,
            };
          })
        )
        .handle("createUser", ({ payload }) =>
          Effect.gen(function* () {
            const userId = yield* dbWrite.table("users").insert({
              name: payload.name,
              email: payload.email,
              createdAt: Date.now(),
            });
            
            return { id: userId };
          })
        );
    })
);

export const apiLive = Layer.provide(
  HttpApiBuilder.api(api),
  UsersApiLive
);

Mounting the HTTP API

Mount your HTTP API in Convex:
convex/http.ts
import { HttpApi } from "@confect/server";
import { apiLive } from "../confect/http";

export default HttpApi.make({
  "/api/": {
    apiLive,
  },
});
The HTTP API will be available at https://your-deployment.convex.site/api/

OpenAPI Documentation

Confect automatically generates OpenAPI documentation accessible at /docs:
export default HttpApi.make({
  "/api/": {
    apiLive,
    scalar: {
      title: "My API Documentation",
      description: "Complete API reference",
    },
  },
});
Documentation will be available at: https://your-deployment.convex.site/api/docs
Scalar provides an interactive API explorer where you can test endpoints directly from the browser.

Request and Response Types

Path Parameters

static readonly GetPost = HttpApiSchema.get(
  "getPost",
  "/posts/:postId"
).pipe(
  HttpApiSchema.setPath(
    Schema.Struct({
      postId: Schema.String,
    })
  )
);

Query Parameters

static readonly ListPosts = HttpApiSchema.get(
  "listPosts",
  "/posts"
).pipe(
  HttpApiSchema.setUrlParams(
    Schema.Struct({
      limit: Schema.optional(Schema.NumberFromString),
      offset: Schema.optional(Schema.NumberFromString),
      author: Schema.optional(Schema.String),
    })
  )
);

Request Body

static readonly UpdatePost = HttpApiSchema.patch(
  "updatePost",
  "/posts/:postId"
).pipe(
  HttpApiSchema.setPayload(
    Schema.Struct({
      title: Schema.optional(Schema.String),
      content: Schema.optional(Schema.String),
    })
  )
);

Response Headers

static readonly GetPost = HttpApiSchema.get(
  "getPost",
  "/posts/:postId"
).pipe(
  HttpApiSchema.setHeaders(
    Schema.Struct({
      "X-Request-Id": Schema.String,
    })
  )
);

Error Handling

Define custom error responses:
class NotFoundError extends Schema.TaggedError<NotFoundError>()()
  "NotFoundError",
  {
    resource: Schema.String,
    id: Schema.String,
  }
) {}

static readonly GetUser = HttpApiSchema.get(
  "getUser",
  "/users/:id"
).pipe(
  HttpApiSchema.setError(
    Schema.Union(
      NotFoundError,
      Schema.Struct({
        _tag: Schema.Literal("ValidationError"),
        message: Schema.String,
      })
    )
  )
);

// In handler
handle("getUser", ({ path }) =>
  Effect.gen(function* () {
    const user = yield* db
      .table("users")
      .get(path.id as Id<"users">)
      .pipe(
        Effect.catchTag("GetByIdFailure", () =>
          Effect.fail(
            new NotFoundError({
              resource: "user",
              id: path.id,
            })
          )
        )
      );
    
    return user;
  })
)

Middleware

Add middleware for cross-cutting concerns:
import { HttpMiddleware, HttpServerRequest, HttpServerResponse } from "@effect/platform";

const corsMiddleware = (app: HttpApp.Default) =>
  HttpMiddleware.make((request) =>
    Effect.gen(function* () {
      const response = yield* app(request);
      
      return response.pipe(
        HttpServerResponse.setHeaders({
          "Access-Control-Allow-Origin": "*",
          "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
          "Access-Control-Allow-Headers": "Content-Type, Authorization",
        })
      );
    })
  );

export default HttpApi.make({
  "/api/": {
    apiLive,
    middleware: corsMiddleware,
  },
});

Authentication Middleware

const authMiddleware = (app: HttpApp.Default) =>
  HttpMiddleware.make((request) =>
    Effect.gen(function* () {
      const auth = yield* HttpServerRequest.HttpServerRequest;
      const header = yield* auth.headers.get("Authorization");
      
      if (!header || !header.startsWith("Bearer ")) {
        return yield* HttpServerResponse.unauthorized(
          Schema.Struct({ error: Schema.Literal("Unauthorized") })
        );
      }
      
      const token = header.slice(7);
      // Validate token...
      
      return yield* app(request);
    })
  );

Logging Middleware

import { Console } from "effect";

const loggingMiddleware = (app: HttpApp.Default) =>
  HttpMiddleware.make((request) =>
    Effect.gen(function* () {
      const req = yield* HttpServerRequest.HttpServerRequest;
      const start = Date.now();
      
      yield* Console.log(`${req.method} ${req.url}`);
      
      const response = yield* app(request);
      
      const duration = Date.now() - start;
      yield* Console.log(
        `${req.method} ${req.url} ${response.status} ${duration}ms`
      );
      
      return response;
    })
  );

Full Example

Here’s a complete HTTP API example:
confect/api/posts.ts
import { HttpApiBuilder, HttpApiSchema } from "@effect/platform";
import { Schema } from "effect";

class PostsApi extends HttpApiSchema.make("posts") {
  static readonly List = HttpApiSchema.get("list", "/posts").pipe(
    HttpApiSchema.setUrlParams(
      Schema.Struct({
        limit: Schema.optional(Schema.NumberFromString),
        cursor: Schema.optional(Schema.String),
      })
    ),
    HttpApiSchema.setSuccess(
      Schema.Struct({
        posts: Schema.Array(
          Schema.Struct({
            id: Schema.String,
            title: Schema.String,
            excerpt: Schema.String,
          })
        ),
        nextCursor: Schema.NullOr(Schema.String),
      })
    )
  );
  
  static readonly Get = HttpApiSchema.get("get", "/posts/:id").pipe(
    HttpApiSchema.setPath(
      Schema.Struct({
        id: Schema.String,
      })
    ),
    HttpApiSchema.setSuccess(
      Schema.Struct({
        id: Schema.String,
        title: Schema.String,
        content: Schema.String,
        authorId: Schema.String,
        createdAt: Schema.Number,
      })
    )
  );
  
  static readonly Create = HttpApiSchema.post("create", "/posts").pipe(
    HttpApiSchema.setPayload(
      Schema.Struct({
        title: Schema.String,
        content: Schema.String,
      })
    ),
    HttpApiSchema.setSuccess(
      Schema.Struct({
        id: Schema.String,
      })
    )
  );
}

const PostsApiLive = HttpApiBuilder.group(
  PostsApi,
  "posts",
  (handlers) =>
    Effect.gen(function* () {
      const db = yield* DatabaseReader<typeof databaseSchema>();
      const dbWrite = yield* DatabaseWriter<typeof databaseSchema>();
      const auth = yield* Auth;
      
      return handlers
        .handle("list", ({ urlParams }) =>
          Effect.gen(function* () {
            const result = yield* db
              .table("posts")
              .paginate({
                numItems: urlParams.limit ?? 10,
                cursor: urlParams.cursor ?? null,
              });
            
            return {
              posts: result.page.map((post) => ({
                id: post._id,
                title: post.title,
                excerpt: post.content.slice(0, 200),
              })),
              nextCursor: result.continueCursor,
            };
          })
        )
        .handle("get", ({ path }) =>
          Effect.gen(function* () {
            const post = yield* db
              .table("posts")
              .get(path.id as Id<"posts">);
            
            return {
              id: post._id,
              title: post.title,
              content: post.content,
              authorId: post.authorId,
              createdAt: post.createdAt,
            };
          })
        )
        .handle("create", ({ payload }) =>
          Effect.gen(function* () {
            const identity = yield* auth.getUserIdentity;
            
            const postId = yield* dbWrite.table("posts").insert({
              title: payload.title,
              content: payload.content,
              authorId: identity.subject,
              createdAt: Date.now(),
            });
            
            return { id: postId };
          })
        );
    })
);

export const apiLive = Layer.provide(
  HttpApiBuilder.api(api),
  PostsApiLive
);

Best Practices

1

Use Schema Validation

Define comprehensive schemas for all inputs and outputs to catch errors early.
2

Version Your API

Include API version in the path (e.g., /api/v1/) for backward compatibility.
3

Handle Errors Gracefully

Return appropriate HTTP status codes and error messages.
4

Document Thoroughly

Add descriptions to your endpoints for better OpenAPI documentation.
HTTP endpoints run as actions and don’t have access to DatabaseWriter. Use MutationRunner to modify data.

Next Steps

Functions

Learn about queries, mutations, and actions

Authentication

Add authentication to your API

Storage

Handle file uploads

Node Actions

Use Node.js in actions