one-api-routes
API Routes
Official docs: Routing
API routes are server-only HTTP endpoints. Name files with the +api.ts suffix.
When to Use
- REST API endpoints
- Webhook handlers
- Form submissions
- Server-side operations (database writes, external API calls)
- Authentication endpoints
When NOT to Use
- Fetching data for a page — use loaders instead
- Client-side logic — use regular components
- Static data — use SSG with loaders
File Structure
app/
api/
route+api.ts GET /api
users/
route+api.ts GET/POST /api/users
[id]+api.ts GET/PUT/DELETE /api/users/:id
webhooks/
stripe+api.ts POST /api/webhooks/stripe
Basic Usage
Export named functions for HTTP methods:
// app/api/health+api.ts
export function GET() {
return Response.json({ status: 'ok' })
}
HTTP Methods
// app/api/users/route+api.ts
export async function GET(request: Request) {
const users = await db.users.findMany()
return Response.json(users)
}
export async function POST(request: Request) {
const body = await request.json()
const user = await db.users.create(body)
return Response.json(user, { status: 201 })
}
// app/api/users/[id]+api.ts
export async function GET(request: Request, { params }: { params: { id: string } }) {
const user = await db.users.find(params.id)
if (!user) {
return Response.json({ error: 'Not found' }, { status: 404 })
}
return Response.json(user)
}
export async function PUT(request: Request, { params }: { params: { id: string } }) {
const body = await request.json()
const user = await db.users.update(params.id, body)
return Response.json(user)
}
export async function DELETE(request: Request, { params }: { params: { id: string } }) {
await db.users.delete(params.id)
return new Response(null, { status: 204 })
}
Default Handler
A default export catches all HTTP methods:
// app/api/proxy+api.ts
export default async function handler(request: Request) {
// handles GET, POST, PUT, DELETE, etc.
return fetch(`https://upstream.api${new URL(request.url).pathname}`, {
method: request.method,
headers: request.headers,
body: request.body,
})
}
Request
Standard Web API Request object:
export async function POST(request: Request) {
// JSON body
const json = await request.json()
// Form data
const form = await request.formData()
// Raw text
const text = await request.text()
// Query params
const url = new URL(request.url)
const q = url.searchParams.get('q')
// Headers
const auth = request.headers.get('Authorization')
}
Response
Standard Web API Response object:
// JSON
return Response.json({ data: 'value' })
// with status
return Response.json({ error: 'bad request' }, { status: 400 })
// with headers
return new Response(JSON.stringify(data), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=3600',
},
})
// redirect
return Response.redirect('/login', 302)
// no content
return new Response(null, { status: 204 })
Typed Routes
Use Endpoint type for typed params:
import type { Endpoint } from 'one'
export type MyEndpoint = Endpoint<'/api/users/[id]'>
export async function GET(
request: Request,
{ params }: { params: MyEndpoint['params'] }
) {
// params.id is typed
}
Or use createAPIRoute:
import { createAPIRoute } from 'one'
export const GET = createAPIRoute<'/api/users/[id]'>(
async (request, { params }) => {
return Response.json({ id: params.id })
}
)
Common Patterns
Authentication middleware
async function requireAuth(request: Request) {
const token = request.headers.get('Authorization')?.replace('Bearer ', '')
if (!token) {
throw Response.json({ error: 'Unauthorized' }, { status: 401 })
}
return verifyToken(token)
}
export async function GET(request: Request) {
const user = await requireAuth(request)
return Response.json({ user })
}
Error handling
export async function POST(request: Request) {
try {
const body = await request.json()
const result = await processData(body)
return Response.json(result)
} catch (error) {
return Response.json(
{ error: error instanceof Error ? error.message : 'Internal error' },
{ status: 500 }
)
}
}
Streaming response
export async function GET() {
const stream = new ReadableStream({
async start(controller) {
for await (const chunk of generateChunks()) {
controller.enqueue(new TextEncoder().encode(chunk))
}
controller.close()
},
})
return new Response(stream, {
headers: { 'Content-Type': 'text/event-stream' },
})
}
Rules
- API route files must end with
+api.ts(or.js) - API routes are server-only — they never ship to the client bundle
- Use standard Web API Request/Response — no framework-specific wrappers
- Dynamic route params use the same
[param]syntax as page routes - Do not export React components from API route files
More from onejs/skills
one-deployment
Deploy One framework apps to Node servers, Vercel, Cloudflare Workers, or as static sites. Covers build, serve, cluster mode, security scanning, and platform-specific configuration.
1one-guides
Guides and recipes for One framework. Covers authentication, images, CSS optimization, dark mode, Tamagui, MDX, OpenGraph images, ISR, skew protection, native iOS builds, EAS, CRA migration, and common issues.
1one-render-modes
Configure render modes (SSG, SSR, SPA, API) in One framework. Use when choosing how pages are rendered, setting up static generation, or mixing render strategies.
1one-loaders
Server-side data loading in One framework. Use when fetching data for pages, implementing caching, redirects, response headers, ISR, refetching, or file-driven content with loaders.
1building-with-one
Complete guide for building full-stack React apps with One framework. Covers routing, layouts, navigation, components, hooks, typed routes, styling, configuration, devtools, and cross-platform (web + native) development.
1