Astro's API Routes feature allows you to create server-side API endpoints in your Astro project for handling data requests, authentication, database operations, and more.
Basic Concepts:
API routes are located in the src/pages/api/ directory, where each file corresponds to an API endpoint.
Creating API Routes:
typescript// src/pages/api/hello.ts export async function GET(context) { return new Response(JSON.stringify({ message: 'Hello, World!' }), { headers: { 'Content-Type': 'application/json', }, }); }
Supported HTTP Methods:
typescript// src/pages/api/users.ts export async function GET(context) { const users = await fetchUsers(); return new Response(JSON.stringify(users), { headers: { 'Content-Type': 'application/json' }, }); } export async function POST(context) { const body = await context.request.json(); const newUser = await createUser(body); return new Response(JSON.stringify(newUser), { status: 201, headers: { 'Content-Type': 'application/json' }, }); } export async function PUT(context) { const body = await context.request.json(); const updatedUser = await updateUser(body); return new Response(JSON.stringify(updatedUser), { headers: { 'Content-Type': 'application/json' }, }); } export async function DELETE(context) { const { id } = context.params; await deleteUser(id); return new Response(null, { status: 204 }); }
Dynamic Route Parameters:
typescript// src/pages/api/users/[id].ts export async function GET(context) { const { id } = context.params; const user = await fetchUserById(id); if (!user) { return new Response(JSON.stringify({ error: 'User not found' }), { status: 404, headers: { 'Content-Type': 'application/json' }, }); } return new Response(JSON.stringify(user), { headers: { 'Content-Type': 'application/json' }, }); }
Request and Response Handling:
typescript// src/pages/api/search.ts export async function POST(context) { try { // Get request body const body = await context.request.json(); const { query } = body; // Get query parameters const url = new URL(context.request.url); const limit = parseInt(url.searchParams.get('limit') || '10'); // Get request headers const authHeader = context.request.headers.get('Authorization'); // Get cookies const sessionCookie = context.cookies.get('session'); // Process business logic const results = await search(query, limit); // Return response return new Response(JSON.stringify({ results }), { status: 200, headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=3600', }, }); } catch (error) { return new Response(JSON.stringify({ error: 'Internal Server Error' }), { status: 500, headers: { 'Content-Type': 'application/json' }, }); } }
Authentication Example:
typescript// src/pages/api/protected.ts export async function GET(context) { const token = context.request.headers.get('Authorization'); if (!token) { return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401, headers: { 'Content-Type': 'application/json' }, }); } const user = await verifyToken(token); if (!user) { return new Response(JSON.stringify({ error: 'Invalid token' }), { status: 403, headers: { 'Content-Type': 'application/json' }, }); } const data = await fetchProtectedData(user.id); return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' }, }); }
File Upload:
typescript// src/pages/api/upload.ts export async function POST(context) { try { const formData = await context.request.formData(); const file = formData.get('file') as File; if (!file) { return new Response(JSON.stringify({ error: 'No file provided' }), { status: 400, headers: { 'Content-Type': 'application/json' }, }); } // Validate file type const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']; if (!allowedTypes.includes(file.type)) { return new Response(JSON.stringify({ error: 'Invalid file type' }), { status: 400, headers: { 'Content-Type': 'application/json' }, }); } // Validate file size (max 5MB) const maxSize = 5 * 1024 * 1024; if (file.size > maxSize) { return new Response(JSON.stringify({ error: 'File too large' }), { status: 400, headers: { 'Content-Type': 'application/json' }, }); } // Save file const url = await uploadFile(file); return new Response(JSON.stringify({ url }), { status: 201, headers: { 'Content-Type': 'application/json' }, }); } catch (error) { return new Response(JSON.stringify({ error: 'Upload failed' }), { status: 500, headers: { 'Content-Type': 'application/json' }, }); } }
Using Database:
typescript// src/pages/api/posts.ts import { db } from '../../lib/db'; export async function GET(context) { const url = new URL(context.request.url); const page = parseInt(url.searchParams.get('page') || '1'); const limit = parseInt(url.searchParams.get('limit') || '10'); const offset = (page - 1) * limit; const posts = await db.post.findMany({ take: limit, skip: offset, orderBy: { createdAt: 'desc' }, }); const total = await db.post.count(); return new Response(JSON.stringify({ posts, pagination: { page, limit, total, totalPages: Math.ceil(total / limit), }, }), { headers: { 'Content-Type': 'application/json' }, }); } export async function POST(context) { const body = await context.request.json(); const { title, content, authorId } = body; const post = await db.post.create({ data: { title, content, authorId, }, }); return new Response(JSON.stringify(post), { status: 201, headers: { 'Content-Type': 'application/json' }, }); }
Error Handling:
typescript// src/lib/api-error.ts export class ApiError extends Error { constructor( public statusCode: number, message: string, public code?: string ) { super(message); this.name = 'ApiError'; } } export function handleApiError(error: unknown) { if (error instanceof ApiError) { return new Response( JSON.stringify({ error: error.message, code: error.code, }), { status: error.statusCode, headers: { 'Content-Type': 'application/json' }, } ); } console.error('Unexpected error:', error); return new Response( JSON.stringify({ error: 'Internal Server Error' }), { status: 500, headers: { 'Content-Type': 'application/json' }, } ); }
typescript// src/pages/api/data.ts import { ApiError, handleApiError } from '../../lib/api-error'; export async function GET(context) { try { const data = await fetchData(); if (!data) { throw new ApiError(404, 'Data not found', 'NOT_FOUND'); } return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json' }, }); } catch (error) { return handleApiError(error); } }
CORS Configuration:
typescript// src/pages/api/cors-example.ts export async function OPTIONS(context) { return new Response(null, { status: 204, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', 'Access-Control-Max-Age': '86400', }, }); } export async function GET(context) { const data = await fetchData(); return new Response(JSON.stringify(data), { headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', }, }); }
Best Practices:
- Use appropriate HTTP methods (GET, POST, PUT, DELETE)
- Implement proper error handling and status codes
- Validate and sanitize input data
- Use TypeScript for type safety
- Implement authentication and authorization
- Add appropriate logging
- Use environment variables for sensitive information
- Implement rate limiting to prevent abuse
- Use appropriate caching strategies
- Write tests to ensure API reliability
Astro's API Routes feature provides powerful server-side capabilities for building full-stack applications while maintaining Astro's simplicity and performance benefits.