Your first handler
Three packages composed into a request handler that never throws, and what each line of it demonstrates.
Last updated 2026-09-21
Here is a handler that accepts a markdown document, validates its frontmatter, and answers with
either the frontmatter or the line the document went wrong on. It uses three packages and has
no try in it.
npm add @sdxc/markdown @sdxc/response @sdxc/result
import { Markdown } from "@sdxc/markdown";
import { badRequest, methodNotAllowed, ok } from "@sdxc/response";
import { isFailure } from "@sdxc/result";
import * as s from "remix/data-schema";
let Frontmatter = s.object({
title: s.string(),
tags: s.optional(s.array(s.string())),
});
export default {
async fetch(request: Request): Promise<Response> {
if (request.method !== "POST") return methodNotAllowed({ error: "Send a POST" });
let parsed = Markdown.parse(await request.text(), { frontmatter: Frontmatter });
if (isFailure(parsed)) {
return badRequest({
error: parsed.error.message,
line: parsed.error.position?.start.line,
});
}
return ok({ frontmatter: parsed.data.frontmatter });
},
};
What each part is doing
Markdown.parse returns a Result. It does not throw on malformed input and it does not
return null. isFailure narrows the union, so the compiler will not let you reach
parsed.data on the branch where there is none. This is the shape every fallible entry point
in the collection has — see Result everywhere.
The schema is a Standard Schema. s.object here comes from remix/data-schema, but the
option accepts any library implementing the same interface, and the parsed frontmatter is typed
from the schema you passed. See
Standard Schema validation.
The error carries a position. parsed.error.position points at where in the document the
parse gave up, so the response tells the client which line to fix instead of that something,
somewhere, was wrong.
The response helpers name the status. ok, badRequest and methodNotAllowed are
@sdxc/response: each writes its status, its status text and its
JSON content type, and merges an ok field into the body so a client has one field to branch
on.
Running it
The handler is a plain object with a fetch method, which is what a Cloudflare Worker, Deno,
Bun and Node's own server all accept. Nothing in it is framework-specific, and nothing in it is
platform-specific either.
Where to go next
Resulteverywhere — the shape, and how to work with it without writingisFailureon every line.Subpath exports — why some of these imports have a second segment.