Overview
Motia makes it easy to build type-safe REST APIs with automatic validation and error handling. This guide walks you through creating production-ready API endpoints.
Your first API endpoint
Let’s start with a simple GET endpoint that returns a greeting.
Create your step file
Create a new file 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' ],
flows: [ 'hello-world-flow' ],
} as const satisfies StepConfig
export const handler : Handlers < typeof config > = async ( _ , { enqueue , logger }) => {
const appName = 'My 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 ,
},
}
}
Test your endpoint
Start your Motia server and test the endpoint: curl http://localhost:3000/hello
You’ll get a response like: {
"message" : "Hello request received! Check logs for processing." ,
"status" : "processing" ,
"appName" : "My App"
}
POST endpoints with validation
Now let’s create a POST endpoint with request body validation.
import type { Handlers , StepConfig } from 'motia'
import { z } from 'zod'
export const config = {
name: 'CreatePetOrder' ,
description: 'Create a pet and optionally place a food order' ,
flows: [ 'pet-store' ],
triggers: [
{
type: 'http' ,
method: 'POST' ,
path: '/pets' ,
bodySchema: z . object ({
pet: z . object ({
name: z . string (),
photoUrl: z . string (). url (),
}),
foodOrder: z
. object ({
quantity: z . number (). positive (),
})
. optional (),
}),
responseSchema: {
200 : z . object ({
id: z . string (),
name: z . string (),
photoUrl: z . string (),
traceId: z . string (),
}),
},
},
],
enqueues: [ 'process-food-order' ],
} as const satisfies StepConfig
export const handler : Handlers < typeof config > = async (
request ,
{ logger , traceId , enqueue }
) => {
logger . info ( 'Processing pet creation' , { body: request . body })
const { pet , foodOrder } = request . body || {}
// Create pet record
const newPetRecord = await createPet ( pet )
// Enqueue food order processing if provided
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 }
}
}
Motia automatically validates the request body against your bodySchema. Invalid requests receive a 400 response before your handler runs.
Multiple response schemas
Define different response schemas for different status codes:
triggers : [
{
type: 'http' ,
method: 'POST' ,
path: '/orders/manual' ,
bodySchema: z . object ({
user: z . object ({
verified: z . boolean (),
}),
amount: z . number (),
description: z . string (),
}),
responseSchema: {
200 : z . object ({
message: z . string (),
orderId: z . string (),
processedBy: z . string (),
}),
403 : z . object ({
error: z . string (),
}),
},
},
]
Then in your handler:
export const handler : Handlers < typeof config > = async ({ request }, ctx ) => {
const { user , amount , description } = request . body
if ( ! user . verified ) {
return {
status: 403 ,
body: { error: 'User must be verified to place orders' },
}
}
const orderId = `order- ${ Date . now () } `
return {
status: 200 ,
body: {
message: 'Order processed successfully' ,
orderId ,
processedBy: 'api' ,
},
}
}
Error handling
Motia provides automatic error handling, but you can also handle errors explicitly:
export const handler : Handlers < typeof config > = async ({ request }, ctx ) => {
try {
const result = await externalApiCall ( request . body )
return {
status: 200 ,
body: { success: true , data: result },
}
} catch ( error ) {
ctx . logger . error ( 'External API call failed' , { error })
return {
status: 500 ,
body: {
error: 'Failed to process request' ,
message: error . message ,
},
}
}
}
Conditional triggers
Add conditions to control when your API endpoint is triggered:
import type { TriggerCondition } from 'motia'
const isVerifiedUser : TriggerCondition <{
user : { verified : boolean }
amount : number
}> = ( input , ctx ) => {
if ( ctx . trigger . type !== 'http' || ! input ) return false
return input . body . user . verified === true
}
export const config = {
name: 'ProcessVerifiedOrder' ,
triggers: [
{
type: 'http' ,
method: 'POST' ,
path: '/orders/verified' ,
bodySchema: z . object ({
user: z . object ({ verified: z . boolean () }),
amount: z . number (),
}),
condition: isVerifiedUser ,
},
],
} as const satisfies StepConfig
Multi-trigger steps
Your step can respond to both HTTP requests and queue events:
import { http , queue , step } from 'motia'
import { z } from 'zod'
const orderSchema = z . object ({
email: z . string (). email (),
quantity: z . number (),
petId: z . string (),
})
export const stepConfig = {
name: 'ProcessOrder' ,
flows: [ 'orders' ],
triggers: [
queue ( 'process-food-order' , { input: orderSchema }),
http ( 'POST' , '/process-food-order' , { bodySchema: orderSchema }),
],
enqueues: [ 'notification' ],
}
export const { config , handler } = step ( stepConfig , async ( _input , ctx ) => {
const data = ctx . getData ()
ctx . logger . info ( 'Processing order' , {
input: data ,
triggerType: ctx . trigger . type ,
})
const order = await createOrder ( data )
await ctx . state . set ( 'orders' , order . id , order )
// Different response based on trigger type
return ctx . match ({
http : async () => ({
status: 200 ,
body: { success: true , order },
}),
queue : async () => {
// Queue triggers don't need HTTP response
ctx . logger . info ( 'Order processed from queue' )
},
})
})
Best practices
Use Zod for validation Define schemas for all inputs and outputs. Motia automatically validates requests.
Log important events Use ctx.logger to log key operations. Logs are automatically traced.
Return proper status codes Use appropriate HTTP status codes: 200 for success, 400 for bad requests, 500 for errors.
Handle errors gracefully Catch exceptions and return meaningful error messages to clients.
Next steps
Background Jobs Learn how to process work asynchronously with queues
Real-time Streaming Add WebSocket and SSE support for real-time updates