Jumentix
REST API blueprint

Contract-first REST APIs without domain lock-in

Use OpenAPI 3.1 to bind request validation, handlers, controllers, SDK clients, and documentation.

Jumentix open-source mascot

What this blueprint gives your team

  • Native adapters for multiple Node.js HTTP runtimes
  • Swagger documentation and static asset serving
  • Two validation layers: interface and domain
export class TaskController {
  constructor(private readonly createTask: CreateTaskUseCase) {}

  async create(input: CreateTaskInput): Promise<TaskOutput> {
    return this.createTask.execute(input);
  }
}

From zero to first MVP

A practical launch sequence for this blueprint

Use this path when the first MVP must expose predictable CRUD or integration behavior that another system, admin screen, or frontend can call immediately.

Choose the first resource

Model one resource such as Task with Category ownership, required fields, validation, and the first create/list operations.

  • Write the Category and Task types with required fields and the completed flag.
  • Seed the work category so the first command has a valid owner.
  • Pick create and list as the only MVP operations.
  • Write acceptance criteria: empty title returns 400, unknown category returns 404.

MVP output: a tiny OpenAPI surface with one command and one query.

type Category = { id: string; name: string };
type Task = { id: string; title: string; categoryId: string; completed: boolean };

const categories = new Map<string, Category>([
  ['work', { id: 'work', name: 'Work' }]
]);
const tasks = new Map<string, Task>();

Definition of done for the first MVP

  • The OpenAPI contract and the running handler describe the same routes and schemas.
  • A typed client can create and list tasks without reading server source code.
  • Invalid input fails with 400 and unknown categories with 404 before persistence.
  • Unit tests, route checks, and a captured smoke request prove the slice before the demo.

Deliberately out of scope

  • Bulk operations and reporting endpoints.
  • Webhooks and outbound event delivery.
  • Rate limiting, authentication, and API versioning policy.
  • Production database adapter — the swap happens after first feedback.

MVP scope

Keep the first release small enough to prove

One resource family

Task and Category are enough to prove CRUD, filtering, validation, and ownership.

Domain slice

One primary route group

Create, list, update status, and fetch by id before adding reporting or bulk operations.

API slice

One adapter profile

Start with in-memory or local SQL, then swap the repository adapter after feedback.

Runtime slice
Proof areaQuestion to answerJumentix mechanismMVP evidence
ContractCan another client understand the API without reading source code?OpenAPI 3.1, route check, schema examples.Docs and route metadata match the handler.
BehaviorDoes the first workflow enforce validation and domain rules?Controller/use-case tests and error contract checks.Invalid input fails before persistence; domain errors stay explicit.
AdoptionCan a frontend or partner call it today?Generated REST client and copyable request example.A first consumer can create and list records.

Practical implementation

Complete code for the first working slice

These examples keep Category and Task as the product vocabulary and show the controller, contract, client, worker, or state layer needed to reach a runnable MVP.

type Category = { id: string; name: string };
type Task = { id: string; title: string; categoryId: string; completed: boolean };

const categories = new Map<string, Category>([
  ['work', { id: 'work', name: 'Work' }]
]);
const tasks = new Map<string, Task>();

class CreateTaskUseCase {
  async execute(input: { title: string; categoryId: string }) {
    if (!input.title.trim()) return { status: 400, body: { error: 'title is required' } };
    if (!categories.has(input.categoryId)) return { status: 404, body: { error: 'category not found' } };

    const task: Task = {
      id: crypto.randomUUID(),
      title: input.title,
      categoryId: input.categoryId,
      completed: false
    };
    tasks.set(task.id, task);
    return { status: 201, body: task };
  }
}

class TaskController {
  constructor(private readonly createTask: CreateTaskUseCase) {}

  async create(request: Request) {
    const input = await request.json() as { title: string; categoryId: string };
    const response = await this.createTask.execute(input);
    return Response.json(response.body, { status: response.status });
  }

  async list() {
    return Response.json([...tasks.values()]);
  }
}

const controller = new TaskController(new CreateTaskUseCase());

export async function handleRequest(request: Request) {
  const url = new URL(request.url);
  if (request.method === 'POST' && url.pathname === '/tasks') return controller.create(request);
  if (request.method === 'GET' && url.pathname === '/tasks') return controller.list();
  return Response.json({ error: 'not found' }, { status: 404 });
}

Build the product. Keep the architecture.

Explore the source, run the factory locally, and turn your next Node.js service into a repeatable platform capability.