Motia Icon

API Endpoints

Learn how to create HTTP API endpoints with Motia

What You'll Build

A pet management API with these endpoints:

  • POST /pets - Create a new pet
  • GET /pets - List all pets
  • GET /pets/:id - Get a specific pet
  • PUT /pets/:id - Update a pet
  • DELETE /pets/:id - Delete a pet

Getting Started

Clone the example repository:

git clone https://github.com/MotiaDev/build-your-first-app.git
cd build-your-first-app
git checkout api-endpoints

Install dependencies:

npm install

Start the dev server:

npm run dev

Project Structure

package.json
requirements.txt
types.d.ts

Project organization - This example uses the src/ directory. Motia discovers step files from the src/ directory automatically.

Files like features.json and tutorial/tutorial.tsx are only for the interactive tutorial and are not part of Motia's project structure.

All code examples in this guide are available in the build-your-first-app repository.

You can follow this guide to learn how to build a REST API with Motia step by step, or clone the repository and get started immediately.


Creating Your First Endpoint

This tutorial focuses on Motia's capabilities to create complete backend system from APIs to Streaming AI agents step-by-step. Here, we're showcasing writing APIs with Motia Steps - For data persistence, we use a simple JSON file store in the examples. In a real application, you would use a database like PostgreSQL, MongoDB, or any other data store of your choice. The complete store implementation is available in the GitHub repository.

Configuration

Every API endpoint has two parts:

Config - Defines when and how the step runs:

PropertyDescription
nameUnique identifier
triggersArray with a trigger of type: 'api'
triggers[].pathURL path for the endpoint
triggers[].methodHTTP method (GET, POST, PUT, DELETE)

Handler - The function that executes your business logic.

View on GitHub:

src/typescript/create-pet.step.ts
import { type Handlers, type StepConfig } from 'motia'
import { z } from 'zod'
import { TSStore } from './ts-store'
 
const createPetSchema = z.object({
  name: z.string().min(1, 'Name is required'),
  species: z.enum(['dog', 'cat', 'bird', 'other']),
  ageMonths: z.number().int().min(0),
})
 
export const config = {
  name: 'CreatePet',
  description: 'Creates a new pet',
  triggers: [
    { type: 'api', path: '/pets', method: 'POST', bodySchema: createPetSchema },
  ],
  enqueues: [],
  flows: ['PetManagement'],
} as const satisfies StepConfig
 
export const handler: Handlers<typeof config> = async (req, { logger }) => {
  const data = createPetSchema.parse(req.body)
 
  const pet = TSStore.create(data)
 
  logger.info('Pet created', { petId: pet.id })
 
  return { status: 201, body: pet }
}

Testing Your API

You can test your endpoints by sending requests directly to the API:

# Create a pet
curl -X POST http://localhost:3000/pets \
  -H "Content-Type: application/json" \
  -d '{"name": "Max", "species": "dog", "ageMonths": 24}'

Adding GET Endpoints

List All Pets

View on GitHub:

src/typescript/get-pets.step.ts
import { type Handlers, type StepConfig } from 'motia'
import { TSStore } from './ts-store'
 
export const config = {
  name: 'GetPets',
  description: 'Lists all pets',
  triggers: [
    { type: 'api', path: '/pets', method: 'GET' },
  ],
  enqueues: [],
  flows: ['PetManagement'],
} as const satisfies StepConfig
 
export const handler: Handlers<typeof config> = async (req, { logger }) => {
  const pets = TSStore.list()
 
  logger.info('Retrieved all pets', { count: pets.length })
  return { status: 200, body: pets }
}

Testing List All Pets

Test with curl:

# List all pets
curl http://localhost:3000/pets

Get Single Pet

View on GitHub:

src/typescript/get-pet.step.ts
import { type Handlers, type StepConfig } from 'motia'
import { TSStore } from './ts-store'
 
export const config = {
  name: 'GetPet',
  description: 'Gets a specific pet by ID',
  triggers: [
    { type: 'api', path: '/pets/:id', method: 'GET' },
  ],
  enqueues: [],
  flows: ['PetManagement'],
} as const satisfies StepConfig
 
export const handler: Handlers<typeof config> = async (req, { logger }) => {
  const pet = TSStore.get(req.pathParams.id)
 
  if (!pet) {
    logger.warn('Pet not found', { id: req.pathParams.id })
    return { status: 404, body: { message: 'Pet not found' } }
  }
 
  return { status: 200, body: pet }
}

The :id in the path creates a path parameter accessible via req.pathParams.id.

Testing Get Single Pet

Test with curl:

# Get specific pet (replace 1 with an actual pet ID)
curl http://localhost:3000/pets/1

Adding UPDATE Endpoint

View on GitHub:

src/typescript/update-pet.step.ts
import { type Handlers, type StepConfig } from 'motia'
import { z } from 'zod'
import { TSStore } from './ts-store'
 
const updatePetSchema = z.object({
  name: z.string().min(1).optional(),
  status: z.enum(['available', 'pending', 'adopted']).optional(),
  ageMonths: z.number().int().min(0).optional(),
})
 
export const config = {
  name: 'UpdatePet',
  description: 'Updates a pet by ID',
  triggers: [
    { type: 'api', path: '/pets/:id', method: 'PUT', bodySchema: updatePetSchema },
  ],
  enqueues: [],
  flows: ['PetManagement'],
} as const satisfies StepConfig
 
export const handler: Handlers<typeof config> = async (req, { logger }) => {
  const updates = updatePetSchema.parse(req.body)
 
  const pet = TSStore.update(req.pathParams.id, updates)
 
  if (!pet) {
    return { status: 404, body: { message: 'Pet not found' } }
  }
 
  logger.info('Pet updated', { petId: pet.id })
  return { status: 200, body: pet }
}

Testing Update Pet

Test with curl:

# Update a pet (replace 1 with an actual pet ID)
curl -X PUT http://localhost:3000/pets/1 \
  -H "Content-Type: application/json" \
  -d '{"status": "adopted"}'

Adding DELETE Endpoint

View on GitHub:

src/typescript/delete-pet.step.ts
import { type Handlers, type StepConfig } from 'motia'
import { TSStore } from './ts-store'
 
export const config = {
  name: 'DeletePet',
  description: 'Deletes a pet by ID',
  triggers: [
    { type: 'api', path: '/pets/:id', method: 'DELETE' },
  ],
  enqueues: [],
  flows: ['PetManagement'],
} as const satisfies StepConfig
 
export const handler: Handlers<typeof config> = async (req, { logger }) => {
  const deleted = TSStore.remove(req.pathParams.id)
 
  if (!deleted) {
    return { status: 404, body: { message: 'Pet not found' } }
  }
 
  logger.info('Pet deleted', { petId: req.pathParams.id })
  return { status: 204 }
}

DELETE endpoints return 204 No Content on success.

Testing Delete Pet

Test with curl:

# Delete a pet (replace 1 with an actual pet ID)
curl -X DELETE http://localhost:3000/pets/1

As you can see in this example, Motia handles routing, validation, and error handling automatically. With just a few lines of code, you've built a complete REST API with:

  • Automatic routing based on your step configuration
  • Path parameter extraction (/pets/:idreq.pathParams.id)
  • HTTP method handling (GET, POST, PUT, DELETE)
  • Response formatting with proper status codes
  • Built-in error handling and validation

Congratulations! You've successfully created your first API endpoints with Motia. Your pet store API is now ready to handle all CRUD operations.


What's Next?

You now have a working REST API for your pet store! But a complete backend system needs more than just API endpoints. In the next guide, we'll add background jobs using Queue Steps and scheduled tasks with Cron Steps to handle tasks like:

  • SetNextFeedingReminder - Queue jobs that automatically schedule feeding reminders when pets are added or updated
  • Deletion Reaper - Cron jobs that run daily to clean up soft-deleted records and expired data

Let's continue building your complete backend system by adding these background jobs with Queue Steps and scheduled tasks with Cron Steps.