Skip to content

REST Controllers

This section will focus on getting our REST controllers set up to deal with our HTTP endpoint, we shall also cover a basic HTML page to interact with our API and then also host that.

Creating our HTTP endpoint.

Let's create the todo controller.

Each HTTP controller route the data is automatically converted into a Response object for you, we use the toResponse utility method. For customisation simply return your own Response object.

Create a new controller file in ./src/controller/api.ts with the following code:

typescript
import { Body, Controller, Delete, Get, Params, Patch, Post, Put } from '@sodacore/http';
import { PrismaClient } from '../../prisma/prisma/client';
import { Inject } from '@sodacore/di';

@Controller('/api')
export default class TodoApiController {
	@Inject('prisma') private readonly prisma!: PrismaClient;

	@Get('/tasks')
	public async list() {
		return this.prisma.todos.findMany();
	}

	@Post('/tasks')
	public async create(@Body() body: { title: string, description?: string }) {
		const createTodo = await this.prisma.todos.create({
			data: {
				title: body.title,
				description: body.description,
			},
		});
		return createTodo;
	}

	@Get('/tasks/:id')
	public async get(@Params('id') id: number) {
		return await this.prisma.todos.findUnique({
			where: { id },
		});
	}

	@Put('/tasks/:id')
	public async replace(@Params('id') id: number, @Body() body: { title: string, description?: string }) {
		const todo = await this.prisma.todos.update({
			where: { id },
			data: {
				title: body.title,
				description: body.description,
			},
		});
		if (!todo) {
			throw new Error('Todo not found');
		}
		return todo;
	}

	@Patch('/tasks/:id')
	public async update(@Params('id') id: number, @Body() body: { title?: string, description?: string, completed?: boolean }) {
		const todo = await this.prisma.todos.update({
			where: { id },
			data: {
				title: body.title,
				description: body.description,
				completed: body.completed,
			},
		});
		return todo;
	}

	@Delete('/tasks/:id')
	public async delete(@Params('id') id: number) {
		const todo = await this.prisma.todos.delete({
			where: { id },
		});
		return todo;
	}
}

Now that we have our rest controller, we can see we have defined a series of methods to handle our HTTP requests, and we have used the Prisma integration to apply our CRUD operations to the database.

Now if you were to run your application using bun dev, you can access your API at http://localhost:3000/api/todo, which should return an empty array as we have no data in our database yet.

A small note here, the reason that the @Inject('prisma') is used because when Prisma decided to make their client a separate package, we have to use dynamic injection to inject the Prisma client, the issue is the generated client does not use a useful class name, in my example the class was called R and when we use injection, we use the class name as the indentifier, so in most built-in classes, it will work fine, but sometimes for separate packages, you may have to use a string to define it, also note that the injection can work on strings and other data you register into your Registry.

Let's create our frontend.

At the root of your project, create a new directory called public, this will be where we will store our static files that we want to serve to the client.

Because this guide focuses on the Sodacore element rather than the frontend, we won't dicuss the frontend in detail, but we may reference it through out the guide, the files you will need are here (linked):

If you download/copy these files into your public directory that covers the frontend element of this project.

The project is written using Vue 3 via an ESM supported CDN, so no additional dependencies are required.

The code is relatively simple, the index.html contains a basic chat sidebar widget, as well as a todo list, with ways of creating, deleting and marking as completed. The styles.css contains the styling for the page, it's relatively rough, but good enough for our purposes. The main.js file contains the logic to interact with our API, it uses the Fetch API to make requests, and we simply call on the WebSocket API to connect to our WebSocket server.

Serve our static files.

You may have already noticed, that at the moment, the framework does not serve our static files, we need to handle this.

To do this, we can simply create a new controller to handle our static files, to do this, create a new file in ./src/controller/app.ts with the following code:

typescript
import { Controller, Get, Params } from '@sodacore/http';
import { resolve } from 'node:path';
import { file } from 'bun';

@Controller('/')
export default class TodoController {
	private readonly basePath = resolve(process.cwd(), './public');

	@Get('/')
	public async index() {

		// Get the index file.
		const indexPath = resolve(this.basePath, 'index.html');
		const indexFile = file(indexPath);

		// Return the file.
		return new Response(indexFile);
	}

	@Get('/:asset')
	public async get(@Params('asset') asset: string) {
		const assetPath = resolve(this.basePath, asset.replaceAll('..', '')); // Prevent directory traversal attacks.
		if (!assetPath.startsWith(this.basePath)) { // Ensure the asset path is within the public directory, ensuring there are no additional directory traversal attacks.
			return new Response('Forbidden', { status: 403 });
		}
		const assetFile = file(assetPath);
		if (!await assetFile.exists()) {
			return new Response('Not Found', { status: 404 });
		}
		return new Response(assetFile);
	}
}

This controller will handle the requests, firstly, we register a route for the root path for our application, this will return the index.html file directly back to the client, we then add some code for dealing with static assets, this will allow us to serve any files in the public directory, such as our CSS and JavaScript files.

If you were to run bun dev now, you should be able to access your application at http://localhost:3000/, and you should see the index page with the todo list and chat widget, there may be some errors, this is because our code is not finished yet, but you should get a valid page showing with styling.

Next steps

Let's add support for WebSockets, so we can have real-time updates to our todo list and chat widget.

Released under the Apache-2.0 License.