Motia Icon
ConceptsSteps

Api Step

An API step is exposed as an HTTP endpoint that acts as an entry point into your sequence of steps, or flow. It allows external systems or clients to trigger and interact with your flows through a REST API interface. Like any Motia Step, an API Step can be configured to emit events or wait for events to occur.

Config

The following properties are specific to the API Step, in addition to the common step config.

PropTypeDescription
string
The development server api path to expose your api step handler
GET | POST
The HTTP method to use for the development server api endpoint
JSON Schema
In TS/JS we use zod to validate the request body, in Python/Ruby we use json schema to validate the request body.
ApiMiddleware[]
An array of middleware functions to be applied to the request before it reaches the handler. Middleware functions can perform tasks like authentication, logging, rate limiting, etc.

The following examples showcase how to configure an API Step

Using Middleware

API Steps support middleware functions that can be applied to requests before they reach your handler. Middleware functions are completely framework-agnostic and can perform tasks such as:

  • Authentication and authorization
  • Request logging
  • Rate limiting
  • CORS handling
  • Request validation
  • Response transformation

Middleware Function Signature

type ApiMiddleware = (
  req: ApiRequest, 
  ctx: FlowContext, 
  next: () => Promise<ApiResponse>
) => Promise<ApiResponse>

Middleware functions receive:

  • req: The API request object with body, headers, pathParams, and queryParams
  • ctx: The flow context with logger, state, emit, and traceId
  • next: A function to call the next middleware or handler in the chain
    • Call next() to continue to the next middleware or handler
    • The return value of next() is the response from the next middleware or handler
    • You can modify this response before returning it

Example Middleware Usage

import { ApiMiddleware } from 'motia'
 
// Logging middleware
const loggingMiddleware: ApiMiddleware = async (req, ctx, next) => {
  ctx.logger.info('Request received', { path: req.pathParams })
  const start = Date.now()
  
  // Call the next middleware and get its response
  const response = await next()
  
  const duration = Date.now() - start
  ctx.logger.info('Request completed', { duration, status: response.status })
  
  return response
}
 
// Authentication middleware
const authMiddleware: ApiMiddleware = async (req, ctx, next) => {
  const authHeader = req.headers.authorization
  
  if (!authHeader) {
    // Return early without calling next()
    return {
      status: 401,
      body: { error: 'Unauthorized' }
    }
  }
  
  // Continue to the next middleware
  return next()
}
 
export const config = {
  type: 'api',
  name: 'protected-endpoint',
  path: '/api/protected',
  method: 'POST',
  emits: ['USER_ACTION'],
  middleware: [
    loggingMiddleware,
    authMiddleware
  ]
}
 
export const handler = async (req, ctx) => {
  // This handler will only be called if all middleware pass
  return {
    status: 200,
    body: { message: 'Protected data accessed successfully' }
  }
}

Creating Custom Middleware

You can create your own middleware functions:

import { ApiMiddleware } from 'motia'
 
// Request modification middleware
const requestModifierMiddleware: ApiMiddleware = async (req, ctx, next) => {
  // Modify the request before passing it to the next middleware
  req.headers['x-modified-by'] = 'middleware'
  req.body.timestamp = Date.now()
  
  // Call the next middleware in the chain
  return next()
}
 
// Response modification middleware
const responseModifierMiddleware: ApiMiddleware = async (req, ctx, next) => {
  // Call the next middleware in the chain
  const response = await next()
  
  // Modify the response before returning it
  response.headers = {
    ...response.headers,
    'x-powered-by': 'Motia',
  }
  
  return response
}
 
// Error handling middleware
const errorHandlingMiddleware: ApiMiddleware = async (req, ctx, next) => {
  try {
    // Call the next middleware in the chain
    return await next()
  } catch (error) {
    ctx.logger.error('Error in handler', { error })
    return {
      status: 500,
      body: { error: 'Internal server error' }
    }
  }
}
 
// Rate limiter middleware with state
const rateLimiterMiddleware: ApiMiddleware = (() => {
  // Closure to maintain state between requests
  const requests: Record<string, number[]> = {}
  const limit = 100
  const windowMs = 60000 // 1 minute
  
  return async (req, ctx, next) => {
    const ip = req.headers['x-forwarded-for'] || 'unknown-ip'
    const ipStr = Array.isArray(ip) ? ip[0] : ip
    
    const now = Date.now()
    if (!requests[ipStr]) {
      requests[ipStr] = []
    }
    
    // Remove old requests outside the time window
    requests[ipStr] = requests[ipStr].filter(time => now - time < windowMs)
    
    if (requests[ipStr].length >= limit) {
      return {
        status: 429,
        body: { error: 'Too many requests, please try again later' }
      }
    }
    
    // Add current request
    requests[ipStr].push(now)
    
    return next()
  }
})()
  import { ApiRouteConfig, StepHandler } from 'motia'
  import { z } from 'zod'
 
  export const config: ApiRouteConfig = {
    type: 'api',
    name: 'Test state api trigger',
    description: 'test state',
    path: '/test-state',
    method: 'POST',
    emits: ['test-state'],
    bodySchema: z.object({}),
    flows: ['test-state'],
  }
 
  export const handler: StepHandler<typeof config> = async (req, { logger, emit }) => {
    logger.info('[Test State] Received request', req)
 
    await emit({
      topic: 'test-state',
      data: req.body
    })
 
    return {
      status: 200,
      body: { message: 'Success' },
    }
  }
Need help? See our Community Resources for questions, examples, and discussions.

On this page