Create your first app

Start an HTTP server, return a JSON response, reject invalid input and test it without opening a port.

On this pageSource-backed Markdown

Set up the files

Complete Installation first, including Fastify and the development tools. Then copy the shipped example files into your app. This is ordinary application code you can edit, not a second framework repository.

Copy the shipped example
bash
mkdir -p examples
cp -R node_modules/@db3.ai/app/src/server/examples/. examples/

Your application layout

This is your standalone app, not the framework source tree. After copying the two server files and adding the test below, it has this layout. You do not need a packages/app directory or Scout checkout.

Keep the code in examples/ while learning. When you move it into your own source folder, update the start command and test import together. Add a private .env only when a database-backed guide needs it, and exclude it and node_modules/ from Git.

Standalone app after adding the test
txt
my-db3-app/
	package.json
	package-lock.json
	examples/
		createFirstServer.ts
		startFirstServer.ts
	tests/
		server/
			firstApplication.test.ts

Create the application

createFirstServer() returns a server without listening yet. It creates one framework App, attaches cleanup to Fastify, and registers health and greeting routes. Keeping construction separate from listening is what makes it easy to test.

The greeting route validates its parameters and wraps its work in requestContext.run(). Request-owned values stay in that asynchronous context rather than becoming process-global state.

examples/createFirstServer.ts
ts
import Fastify, { type FastifyInstance } from 'fastify';
import { App } from '@db3.ai/app/server';

/**
 * Creates a small HTTP application without opening a port or database.
 *
 * The caller owns listen/close, so the same app can serve requests in a process
 * or be exercised through Fastify.inject in an isolated test.
 *
 * @param greeting - Application-owned greeting, configurable at process boot.
 * @returns HTTP server with framework cleanup attached to its close lifecycle.
 */
export function createFirstServer(greeting = 'Hello'): FastifyInstance {
	const application = new App({ config: { greeting } });
	const server = Fastify();
	server.addHook('onClose', async () => { await application.close(); });
	server.get('/health', async () => ({ status: 'ready' }));
	server.get<{ Params: { name: string } }>('/hello/:name', {
		schema: { params: { type: 'object', required: ['name'], properties: { name: { type: 'string', minLength: 1, maxLength: 80 } } } },
	}, async request => application.requestContext.run(async () => {
		application.requestContext.set('requestId', request.id);
		return { message: `${application.config.get<string>('greeting')}, ${request.params.name}!` };
	}));
	return server;
}

Own the process boundary

The entry point chooses the greeting and port, starts a local listener and closes it on Ctrl-C or SIGTERM. It listens on 127.0.0.1; production binding, HTTPS, supervision and proxy trust need deliberate deployment configuration.

examples/startFirstServer.ts
ts
import { createFirstServer } from './createFirstServer';

const port = Number(process.env.PORT ?? 3000);
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('PORT must be an integer from 1 to 65535.');
const server = createFirstServer(process.env.GREETING ?? 'Hello');

/** Closes the listener and framework resources when the local process stops. */
async function shutdown(): Promise<void> {
	await server.close();
}

process.once('SIGINT', shutdown);
process.once('SIGTERM', shutdown);
try {
	console.log(await server.listen({ port, host: '127.0.0.1' }));
} catch (error) {
	await server.close();
	throw error;
}

Run the app

Run this from your app directory. Leave the process running while you try the requests in another terminal. No database tables are created.

Start locally
bash
npx tsx examples/startFirstServer.ts

Make a request

The health route returns {"status":"ready"}. The greeting returns {"message":"Hello, Ada!"}. An unknown route returns 404; a name longer than 80 characters returns 400.

In a second terminal
bash
curl http://127.0.0.1:3000/health
curl http://127.0.0.1:3000/hello/Ada

Testing

Create a tests/server directory and save the test below as firstApplication.test.ts. It imports the example you copied into examples/. Keep the same folder layout so the relative import resolves.

Run from the application root with the development dependencies from Installation. These are consumer tests, not commands that assume a framework checkout.

tests/server/firstApplication.test.ts
ts
import { expect, it } from 'vitest';
import { createFirstServer } from '../../examples/createFirstServer';

it('serves a useful route and rejects an invalid name without a database or socket', async () => {
	const server = createFirstServer('Welcome');
	try {
		const health = await server.inject('/health');
		expect(health.statusCode).toBe(200);
		expect(health.json()).toEqual({ status: 'ready' });
		const greeting = await server.inject('/hello/Ada');
		expect(greeting.statusCode).toBe(200);
		expect(greeting.json()).toEqual({ message: 'Welcome, Ada!' });
		expect((await server.inject(`/hello/${'x'.repeat(81)}`)).statusCode).toBe(400);
		expect((await server.inject('/missing')).statusCode).toBe(404);
	} finally {
		await server.close();
	}
});

Run your tests

The test uses server.inject() against the same application factory. It covers a successful response, configuration, invalid input and a missing route, then closes the framework. No mock App, TCP port or SQL account is involved.

Run your copied test and check types
bash
npx vitest run tests/server/firstApplication.test.ts
npx tsc --noEmit --target ES2022 --module ESNext --moduleResolution Bundler --types node --skipLibCheck examples/*.ts

Build the next feature

Change the greeting and run the test. Add another route and its success and failure cases. Move your own application code out of examples/ when you settle on a structure; keep the test import in sync.

This is a working backend starting point, not a full SaaS shell. Authentication, durable records, file ownership and background processing belong to the next guides.

Behaviour tested Executed by the documentation maintenance gate
What this does

Runs health, greeting, validation and not-found requests through the real HTTP application.

Expected outputThe guide test passes against the real framework components.
Behaviour testpackages/app/src/server/tests/examples/firstApplication.test.ts

This test command requires the framework repository. Use the walkthrough commands in an installed application.

Environment: Node.js 24 or newer; Fastify. No database, external service or listening port.