Expose over HTTP
You write the service that owns the entity, the controller that mounts
the CRUD routes, and the HTTP module that ties them together. By the end
of this page, curl returns a fresh post from POST /posts and the
same post from GET /posts/:id — once Postgres is wired on page 6.
The service
Section titled “The service”A service is the entity’s single DB gateway. Every read and write goes
through Repo<Posts>; controllers, resolvers, gateways never touch
SeaORM directly. The HTTP layer in this section drives a service that
implements CrudService — that trait is what the #[crud(...)] macro
on the controller expands against.
The service crate here is
nest-rs-seaorm —
it provides CrudService, Repo, ServiceError, and the SeaORM
integration. SeaORM stays the source of truth for the column types and
the active-model semantics.
use nest_rs_core::injectable;use nest_rs_seaorm::CrudService;
use super::entity::{CreatePostDto, Entity as Posts, UpdatePostDto};
#[injectable]#[derive(Default)]pub struct PostsService;
impl CrudService for PostsService { type Entity = Posts; type Create = CreatePostDto; type Update = UpdatePostDto;}#[injectable] marks the struct as a provider — the container builds
it once and shares it as Arc<PostsService>. The CrudService impl
declares the entity and the inputs the controller needs. No custom
methods yet: the generated create / find_by_id / list paths on
the trait are enough for a bare CRUD feature.
Repo::<Posts>::conn() reaches the ambient executor — the pool on safe
routes, the transaction on mutating routes. You don’t pass it in; the
data layer installs it before the handler runs (page 6 makes that
concrete).
Register the service in the port module:
use nest_rs_core::module;
use super::service::PostsService;
#[module(providers = [PostsService])]pub struct PostsModule;The controller
Section titled “The controller”A controller is a struct with #[controller(path = ...)] and a
#[crud(...)] impl block that declares the entity. The macro generates
every CRUD verb — list, page, create, get, update, delete —
from the CrudService impl. You don’t write handler bodies unless you
need domain logic the trait doesn’t cover.
use std::sync::Arc;
use nest_rs_http::{controller, crud};
use crate::posts::{CreatePostDto, Entity as PostEntity, Post, PostsService, UpdatePostDto};
#[controller(path = "/posts")]pub struct PostsController { #[inject] svc: Arc<PostsService>,}
#[crud( service = svc, entity = PostEntity, output = Post, create = CreatePostDto, update = UpdatePostDto,)]impl PostsController {}No guards on the struct — blog is a public API for the tutorial.
When you copy users into api, the
same shape picks up #[use_guards(...)] and hand-written create /
get methods that read the caller’s org from a JWT.
The HTTP module
Section titled “The HTTP module”The feature’s HTTP adapter is a separate module. It imports the port
(PostsModule) and lists the controller.
use nest_rs_core::module;
use super::controller::PostsController;use crate::posts::PostsModule;
#[module( imports = [PostsModule], providers = [PostsController],)]pub struct PostsHttpModule;mod controller;mod module;
pub use controller::PostsController;pub use module::PostsHttpModule;Add the adapter to the feature root:
mod entity;mod module;mod service;
pub mod http;
pub use entity::*;pub use module::PostsModule;pub use service::PostsService;
pub use http::{PostsController, PostsHttpModule};Wire it into the app
Section titled “Wire it into the app”The app root now imports the feature’s HTTP adapter — that’s the only
edit apps/blog/ needs beyond the scaffold from page 1.
use nest_rs_core::module;use nest_rs_http::{HttpConfig, HttpModule};use nest_rs_opentelemetry::OpenTelemetryModule;
use features::posts::PostsHttpModule;
#[module(imports = [ OpenTelemetryModule, HttpModule::for_root(HttpConfig { port: 3005, ..Default::default() }), PostsHttpModule,])]pub struct BlogModule;Add features to apps/blog/Cargo.toml:
[dependencies]nest-rs-core.workspace = truefeatures.workspace = truenest-rs-http.workspace = truenest-rs-opentelemetry = { workspace = true, features = ["http"] }anyhow.workspace = truetokio.workspace = trueThe port 3005 is what the reference workspace pins — yours may differ
if other apps already occupy it when you ran nestrs new blog.
Run it
Section titled “Run it”-
Boot the binary.
Terminal window $ nestrs run dev blogDEBUG nest_rs_http::transport: http transport listening addr=0.0.0.0:3005 -
The controller and the service compile, but you haven’t wired a database yet —
Repo::<Posts>::conn()returns an error. Page 6 plugsDatabaseModulein. For now, confirm the routes mounted.Terminal window $ curl -i http://localhost:3005/postsHTTP/1.1 500 Internal Server Error
The route table has the verbs (GET /posts, GET /posts/:id,
POST /posts, PATCH /posts/:id, DELETE /posts/:id); the 500 is
honest — there’s no ambient DB to reach yet. The shape is right.
Confirm the route table from the boot log: the nest_rs::routes target
prints every mount on startup.
INFO nest_rs::routes: GET /postsINFO nest_rs::routes: POST /postsINFO nest_rs::routes: GET /posts/:idINFO nest_rs::routes: PATCH /posts/:idINFO nest_rs::routes: DELETE /posts/:idWhat you have now
Section titled “What you have now”- A
PostsServicethat owns the entity throughRepo<Posts>. - A
PostsControllerwith the full CRUD surface#[crud]generated. - A
PostsHttpModulemounted in the app — the boot log shows the routes.