Ship an API
This guide walks you through building and shipping a JSON API with Guren. You will scaffold an API-only project, define a database schema, create controllers with validation, and test your endpoints.
Ship an API
This guide walks you through building and shipping a JSON API with Guren. You will scaffold an API-only project, define a database schema, create controllers with validation, and test your endpoints.
Note
For full details on controllers, validation, and middleware, see Controllers and Validation.
Prerequisites
- Bun 1.1 or later
- Docker Desktop (Compose v2) for Postgres
1. Scaffold an API Project
The api blueprint skips Inertia and frontend tooling, giving you a lean JSON API starter:
bunx create-guren-app my-api --blueprint api
cd my-api
bun install
2. Start the Database
docker compose up -d
3. Define Your Schema
Open db/schema.ts and add a table. Here is a simple tasks table:
import { pgTable, serial, text, boolean, timestamp } from 'drizzle-orm/pg-core'
export const tasks = pgTable('tasks', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
completed: boolean('completed').notNull().default(false),
createdAt: timestamp('created_at').defaultNow().notNull(),
})
Generate and run a migration:
bunx guren db:migrate:generate create_tasks
bunx guren db:migrate
4. Create the Model
bunx guren make:model Task
Then wire it to your schema:
import { Model } from '@guren/core'
import { tasks } from '@/db/schema'
export class Task extends Model<typeof tasks> {
static table = tasks
}
5. Create the Controller
bunx guren make:controller TaskController
Add CRUD actions with Zod validation:
import { Controller } from '@guren/core'
import { z } from 'zod'
import { Task } from '@/app/Models/Task'
const CreateTaskSchema = z.object({
title: z.string().min(1).max(255),
})
const UpdateTaskSchema = z.object({
title: z.string().min(1).max(255).optional(),
completed: z.boolean().optional(),
})
const TaskIdSchema = z.object({
id: z.coerce.number().int().positive(),
})
export class TaskController extends Controller {
async index() {
const tasks = await Task.all()
return this.json({ tasks })
}
async show() {
const { id } = this.validateParams(TaskIdSchema)
const task = await Task.findOrFail(id)
return this.json({ task })
}
async store() {
const data = await this.validateBody(CreateTaskSchema)
const task = await Task.create(data)
return this.json({ task }, 201)
}
async update() {
const { id } = this.validateParams(TaskIdSchema)
const data = await this.validateBody(UpdateTaskSchema)
const task = await Task.findOrFail(id)
await task.update(data)
return this.json({ task })
}
async destroy() {
const { id } = this.validateParams(TaskIdSchema)
const task = await Task.findOrFail(id)
await task.delete()
return this.json({ message: 'Deleted' })
}
}
6. Register Routes
Open routes/web.ts (or routes/api.ts in an API-only project) and add:
import { Router } from '@guren/core'
import { TaskController } from '@/app/Http/Controllers/TaskController'
export function registerApiRoutes(router: Router): void {
router.get('/api/tasks', [TaskController, 'index']).name('tasks.index')
router.get('/api/tasks/:id', [TaskController, 'show']).name('tasks.show')
router.post('/api/tasks', [TaskController, 'store']).name('tasks.store')
router.put('/api/tasks/:id', [TaskController, 'update']).name('tasks.update')
router.delete('/api/tasks/:id', [TaskController, 'destroy']).name('tasks.destroy')
}
7. Generate Type Manifests
bun run codegen
8. Test Your Endpoints
Start the dev server:
bun run dev
Then use curl or any HTTP client:
# Create a task
curl -X POST http://localhost:3333/api/tasks \
-H "Content-Type: application/json" \
-d '{"title": "Write docs"}'
# List tasks
curl http://localhost:3333/api/tasks
# Show a task
curl http://localhost:3333/api/tasks/1
# Update a task
curl -X PUT http://localhost:3333/api/tasks/1 \
-H "Content-Type: application/json" \
-d '{"completed": true}'
# Delete a task
curl -X DELETE http://localhost:3333/api/tasks/1
9. Add API Token Authentication
For routes that require authentication, add token-based auth:
bunx guren add auth --api
Then protect routes with the auth:api middleware:
router.middleware('auth:api').group((auth) => {
auth.post('/api/tasks', [TaskController, 'store']).name('tasks.store')
auth.put('/api/tasks/:id', [TaskController, 'update']).name('tasks.update')
auth.delete('/api/tasks/:id', [TaskController, 'destroy']).name('tasks.destroy')
})
Clients include the token in the Authorization header:
curl -X POST http://localhost:3333/api/tasks \
-H "Authorization: Bearer your-api-token" \
-H "Content-Type: application/json" \
-d '{"title": "Authenticated task"}'
Next Steps
- Rate Limiting — protect endpoints from abuse
- API Resources — shape JSON responses with resource classes
- Validation — advanced validation patterns
- Error Handling — customize API error responses