Guide

Build a task API

Create a task and call your API from TypeScript.

On this page

Build a small API that creates a task from a title. You’ll write the handler and call it from a TypeScript script, then see the new task printed in your terminal.

Everything runs in that script for now, so there’s no HTTP server to set up.

Start in an empty directory. Create a package and install the API with Zod for input validation:

Create src/api/ for the three files below. This guide uses Bun to run TypeScript; you can use any of the package managers above to install dependencies.

  1. 1Describe the routeSay what a task looks like and how to create one.

    First, describe the route in a contract. Both the client and server will import this file so they agree on what to send and what to return.

    src/api/contract.ts
    import { defineContract, request, response, route } from '@hulla/api'
    import { z } from 'zod'
    
    const newTask = z.object({
      title: z.string().min(1),
    })
    
    export const task = z.object({
      id: z.string(),
      title: z.string(),
      completed: z.boolean(),
    })
    
    export const contract = defineContract({
      basePath: '/api',
      routes: {
        createTask: route.post('/tasks', {
          body: request.json(newTask),
          responses: {
            201: response.json(task),
          },
        }),
      },
    })

    z.string().min(1) rejects an empty title. The name createTask will also be the name of the client method, so keep it handy for step 3.

  2. 2Add the server behaviorGive the route a handler that creates a task.

    Now write the handler. It receives a validated body, gives the task an ID, and returns it with status 201 (Created).

    src/api/implementation.ts
    import { defineServer } from '@hulla/api/server'
    import { contract } from './contract'
    
    export const implementation = defineServer(contract).implement({
      createTask: ({ body, response }) =>
        response(201, {
          id: crypto.randomUUID(),
          title: body.title,
          completed: false,
        }),
    })

    There is one route to implement, so we need one handler. If you add another route to the contract, TypeScript will ask you to implement that one too.

  3. 3Call the APISend a title and print the task you get back.

    Create a client from the contract and connect it to the implementation with inProcessTransport(). This lets the script call your handler directly, so there is no web server to start.

    src/api/example.ts
    import { createClient } from '@hulla/api/client'
    import { inProcessTransport } from '@hulla/api/in-process'
    import { contract } from './contract'
    import { implementation } from './implementation'
    
    const api = createClient(contract, {
      transport: inProcessTransport(implementation),
    })
    
    const result = await api.createTask({
      body: { title: 'Write the quick start' },
    })
    
    console.log(result.body)

    Run it from the project root:

    Terminal
    bun src/api/example.ts

    The call prints a task shaped like this:

    TypeScript
    {
      id: 'generated-id',
      title: 'Write the quick start',
      completed: false,
    }

    Change the title and run it again. The response should contain your new text and a generated ID.

    This example does not save tasks

    The handler returns a new task on each call. To keep tasks after the script exits, add a database or write them to a file.

    Try an empty title next. Validation should reject the call before your handler creates a task.

You now have a working API call. Where you take it next depends on your app: