Server API

Ecopages provides a powerful server API that allows you to create endpoints and handle server-side logic with ease.

Basic Usage

Create an app.ts file in your project root:

import { createApp } from '@ecopages/core/create-app';
 
const app = await createApp();
 
app.get('/api/hello', async () => {
	return new Response('Hello World');
});
 
await app.start();

A handler can instead live in an endpoint file (*.api.ts, .api.tsx, .api.js or .api.jsx) under src/pages; see Endpoint Files.

Starting the server

Call app.start() after you register routes. It boots the server for the current CLI mode (dev, preview, build, or start).

Pass an optional callback to run logic once the server can accept traffic. It receives { origin, routes }; origin is the resolved base URL with no trailing slash.

await app.start(({ origin, routes }) => {
	// readiness hook
});

The OnAppStartCallback type is exported from @ecopages/core/create-app. The callback receives { origin, routes }; app.start() binds the port, prepares watch-mode resources when enabled, then runs the callback once the server can accept traffic.

Concurrent start() and fetch() calls share app initialization. app.fetch() does not bind a network server. app.stop() closes open connections, WebSocket clients included. Calling it while start() is still running stops the startup and leaves nothing listening; that start() resolves without a server, or rejects with the startup error if the step it was running fails, such as the server failing to bind. After app.stop(), a later start() or fetch() initializes a fresh server adapter. A start() or fetch() called while app.stop() is still running waits for it, and a further app.stop() during that wait cancels it: the start() resolves without a server and the fetch() rejects.

HTTP Methods

The server supports all standard HTTP methods:

app.get('/api/resource', handler);
app.post('/api/resource', handler);
app.put('/api/resource', handler);
app.patch('/api/resource', handler);
app.delete('/api/resource', handler);
app.options('/api/resource', handler);
app.head('/api/resource', handler);
app.query('/api/resource', handler);

app.query() registers the HTTP QUERY method, whose request body carries the query. It is not the handler context's query field, which holds the URL's search parameters.

Route Parameters

You can define dynamic route parameters using the :param syntax:

app.get('/api/users/:id/posts/:postId', async ({ params }) => {
	const { id, postId } = params;
	return new Response(JSON.stringify({ userId: id, postId }));
});

Type Safety

The path string selects the URL of an Explicit Route. It does not type params: without a schema, params is a record of strings with string arrays for catch-all segments, so params.id and a misspelled params.ids type the same way.

To type and validate Params, give the route a schema.params, inline in app.get() and its siblings or through defineApiHandler or a method helper such as defineGet. Each types the handler context from that schema:

import { defineApiHandler, defineGet } from '@ecopages/core';
import { z } from 'zod';
 
const getUserHandler = defineGet({
	path: '/api/users/:id',
	schema: {
		params: z.object({ id: z.string() }),
	},
	handler: async ({ params, response }) => {
		// params is { id: string }
		return response.json({ id: params.id, name: 'Example User' });
	},
});
 
app.add(getUserHandler);
 
const updateUserHandler = defineApiHandler({
	method: 'PUT',
	path: '/api/users/:id',
	schema: {
		params: z.object({ id: z.string() }),
	},
	handler: async ({ params }) => {
		return new Response(JSON.stringify({ id: params.id, message: 'User updated' }));
	},
});
 
app.add(updateUserHandler);

A request whose Params fail the schema gets a 400 response before the handler runs. In the handler, params, body, query and headers take the types of the matching schema fields.

Handler Context: Accessing the Server Instance

Once app.start() has bound a server, the handler context's server property is that Node HTTP server. Its generic handler type is unknown, and it is undefined for an app.fetch() call made before app.start(), so narrow it before using Node APIs.

import { Server } from 'node:http';
 
app.get('/api/server', async ({ response, server }) => {
	if (!(server instanceof Server)) {
		return response.json({ listening: false, address: null });
	}
 
	return response.json({
		listening: server.listening,
		address: server.address(),
	});
});

Use server when a handler needs Node HTTP server state. app.fetch() does not bind one, so a request sent through it before app.start() has no server instance.

Static Site Generation

During static site generation, the server can still handle API requests, making it possible to generate static content from API responses:

import { eco } from '@ecopages/core';
 
export default eco.page({
	staticProps: async ({ appConfig }) => {
		const response = await fetch(`${appConfig.baseUrl}/api/data`);
		const data = await response.json();
		
		return {
			props: { data },
		};
	},
	render: ({ data }) => <pre>{JSON.stringify(data)}</pre>
});

Development Server

The development server includes:

  • Hot Module Replacement (HMR)
  • Automatic route reloading
  • API endpoint hot reloading
  • Static file serving
  • Error handling with detailed stack traces

Production Build

When building for production:

  1. Static routes are pre-rendered
  2. API routes are preserved for server-side handling
  3. Assets are optimized and collected
  4. Development-only code is stripped

Error Handling

The server includes built-in error handling:

app.get('/api/error', async () => {
	throw new Error('Something went wrong');
	// Will return a 500 response with error details in development
	// and a safe error message in production
});

WebSocket Support

Ecopages provides a WebSocket API with dynamic segment support, per-connection context, and automatic HTTP-to-WebSocket upgrades on the Node HTTP host.

See WebSockets for the full guide.

Testing API Endpoints

Use app.fetch() to exercise handlers without a bound network server.

import { createApp } from '@ecopages/core/create-app';
 
const app = await createApp();
 
app.get('/api/hello', async ({ response }) => {
	return response.json({ message: 'Hello World' });
});
 
const response = await app.fetch(new Request('http://localhost/api/hello'));
const data = await response.json();
 
console.log(data); // { message: 'Hello World' }

Configuration

Server configuration can be customized in your eco.config.ts:

import { defineConfig } from '@ecopages/core/config';
import { ecopagesJsxPlugin } from '@ecopages/ecopages-jsx';
 
export default defineConfig({
	rootDir: import.meta.dirname,
	baseUrl: 'http://localhost:3000',
	integrations: [ecopagesJsxPlugin()],
});