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:
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:
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:
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),
})
)
);
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:
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
Use Schema Validation
Define comprehensive schemas for all inputs and outputs to catch errors early.
Version Your API
Include API version in the path (e.g., /api/v1/) for backward compatibility.
Handle Errors Gracefully
Return appropriate HTTP status codes and error messages.
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