Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/motiadev/motia/llms.txt

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

Motia makes building REST APIs simple with HTTP triggers. Every API endpoint is a Step with automatic request validation, type-safe responses, and built-in observability.

Creating your first API endpoint

Create an API endpoint by defining a Step with an HTTP trigger:
// steps/hello-api.step.ts
import type { Handlers, StepConfig } from 'motia'
import { z } from 'zod'

export const config = {
  name: 'HelloAPI',
  description: 'Receives hello request and enqueues event for processing',
  triggers: [
    {
      type: 'http',
      path: '/hello',
      method: 'GET',
      responseSchema: {
        200: z.object({
          message: z.string(),
          status: z.string(),
          appName: z.string(),
        }),
      },
    },
  ],
  enqueues: ['process-greeting'],
} as const satisfies StepConfig

export const handler: Handlers<typeof config> = async (_, { enqueue, logger }) => {
  const appName = 'III App'
  const timestamp = new Date().toISOString()

  logger.info('Hello API endpoint called', { appName, timestamp })

  await enqueue({
    topic: 'process-greeting',
    data: {
      timestamp,
      appName,
      greetingPrefix: process.env.GREETING_PREFIX || 'Hello',
      requestId: Math.random().toString(36).substring(7),
    },
  })

  return {
    status: 200,
    body: {
      message: 'Hello request received! Check logs for processing.',
      status: 'processing',
      appName,
    },
  }
}

Building a REST API with validation

Here’s a complete example building a pet store API with request validation and multiple endpoints:
1

Define your data schemas

Create type-safe schemas for your API:
// services/types.ts
import { z } from 'zod'

export const petSchema = z.object({
  id: z.string(),
  name: z.string(),
  photoUrl: z.string(),
})

export const orderSchema = z.object({
  id: z.string(),
  quantity: z.number(),
  petId: z.string(),
  shipDate: z.string(),
  status: z.enum(['placed', 'approved', 'delivered']),
  complete: z.boolean(),
})

export type Pet = z.infer<typeof petSchema>
export type Order = z.infer<typeof orderSchema>
2

Create the API endpoint

Define an HTTP trigger with body validation:
// steps/api.step.ts
import type { Handlers, StepConfig } from 'motia'
import { z } from 'zod'
import { petStoreService } from './services/pet-store'
import { petSchema } from './services/types'

export const config = {
  name: 'ApiTrigger',
  description: 'Create a pet and optionally order food',
  flows: ['pet-store'],
  triggers: [
    {
      type: 'http',
      method: 'POST',
      path: '/pets',
      bodySchema: z.object({
        pet: z.object({
          name: z.string(),
          photoUrl: z.string(),
        }),
        foodOrder: z
          .object({
            quantity: z.number(),
          })
          .optional(),
      }),
      responseSchema: {
        200: petSchema,
      },
    },
  ],
  enqueues: ['process-food-order'],
} as const satisfies StepConfig

export const handler: Handlers<typeof config> = async (request, { logger, traceId, enqueue }) => {
  logger.info('Processing API Step', { body: request.body })

  const { pet, foodOrder } = request.body || {}
  const newPetRecord = await petStoreService.createPet(pet)

  if (foodOrder) {
    await enqueue({
      topic: 'process-food-order',
      data: {
        quantity: foodOrder.quantity,
        email: 'customer@example.com',
        petId: newPetRecord.id,
      },
    })
  }

  return { status: 200, body: { ...newPetRecord, traceId } }
}
3

Test your API

Start your Motia app and test the endpoint:
curl -X POST http://localhost:8787/pets \
  -H "Content-Type: application/json" \
  -d '{
    "pet": {
      "name": "Fluffy",
      "photoUrl": "https://example.com/fluffy.jpg"
    },
    "foodOrder": {
      "quantity": 2
    }
  }'

Handling different HTTP methods

Create CRUD endpoints with different HTTP methods:
export const config = {
  name: 'TodoAPI',
  triggers: [
    {
      type: 'http',
      method: 'POST',
      path: '/todos',
      bodySchema: z.object({ description: z.string() }),
    },
    {
      type: 'http',
      method: 'GET',
      path: '/todos/:id',
    },
    {
      type: 'http',
      method: 'PUT',
      path: '/todos/:id',
      bodySchema: z.object({ description: z.string().optional(), completed: z.boolean().optional() }),
    },
    {
      type: 'http',
      method: 'DELETE',
      path: '/todos/:id',
    },
  ],
} as const satisfies StepConfig

export const handler: Handlers<typeof config> = async (_, ctx) => {
  return ctx.match({
    http: async ({ request }) => {
      const { method, params } = request
      
      switch (method) {
        case 'POST':
          // Create todo
          const newTodo = await ctx.state.set('todos', generateId(), request.body)
          return { status: 201, body: newTodo }
          
        case 'GET':
          // Get todo
          const todo = await ctx.state.get('todos', params.id)
          return todo ? { status: 200, body: todo } : { status: 404 }
          
        case 'PUT':
          // Update todo
          const updated = await ctx.state.set('todos', params.id, request.body)
          return { status: 200, body: updated }
          
        case 'DELETE':
          // Delete todo
          await ctx.state.delete('todos', params.id)
          return { status: 204 }
      }
    },
  })
}

Response schemas and validation

Define multiple response schemas for different status codes:
export const config = {
  name: 'CreateTodo',
  triggers: [
    http('POST', '/todos', {
      bodySchema: z.object({ 
        description: z.string(),
        dueDate: z.string().optional() 
      }),
      responseSchema: {
        200: z.object({
          id: z.string(),
          description: z.string(),
          createdAt: z.string(),
        }),
        400: z.object({ error: z.string() }),
        500: z.object({ error: z.string() }),
      },
    }),
  ],
} as const satisfies StepConfig

Query parameters and path params

Access query parameters and path parameters from the request:
export const handler: Handlers<typeof config> = async ({ request }, { logger }) => {
  // Path parameters
  const userId = request.params.id
  
  // Query parameters
  const page = request.query.page || '1'
  const limit = request.query.limit || '10'
  
  logger.info('Fetching user data', { userId, page, limit })
  
  return {
    status: 200,
    body: {
      userId,
      page: parseInt(page),
      limit: parseInt(limit),
    },
  }
}

Error handling

Handle errors gracefully with proper HTTP status codes:
export const handler: Handlers<typeof config> = async ({ request }, { logger, state }) => {
  try {
    const { description } = request.body
    
    if (!description) {
      return { status: 400, body: { error: 'Description is required' } }
    }
    
    const todo = await state.set('todos', generateId(), { description })
    return { status: 200, body: todo }
    
  } catch (error) {
    logger.error('Failed to create todo', { error })
    return { status: 500, body: { error: 'Internal server error' } }
  }
}

HTTP triggers

Learn about HTTP trigger configuration

Context API

Access request data and context

Background jobs

Process work asynchronously

State management

Store and retrieve data