A Scala library for rapidly building type-safe CRUD REST APIs with http4s.
- Composable CRUD endpoints with minimal boilerplate
- Support for both open and authenticated routes
- Pagination and filtering via query parameters
- Tagless final design with Cats Effect
- Functional error handling with
EitherT - Built-in JSON serialization via Circe
- CORS support out of the box
Add to your build.sbt:
resolvers += "GitHub Package Registry (sapienapps)" at "https://maven.pkg.github.com/sapienapps/universal-rest" libraryDependencies += "com.sapienapps" %% "universal-rest" % "0.9.6"import cats.Applicative import cats.data.EitherT import com.sapienapps.http4s._ case class UserRepo[F[_]: Applicative]() extends CrudRepository[F, String, User, AppError, Session] { def create(entity: User)(implicit session: Session): EitherT[F, AppError, User] = ??? def get(id: String)(implicit session: Session): EitherT[F, AppError, User] = ??? def update(entity: User)(implicit session: Session): EitherT[F, AppError, User] = ??? def delete(id: String)(implicit session: Session): EitherT[F, AppError, User] = ??? def collection(isCount: Boolean)(implicit session: Session): EitherT[F, AppError, DataResult[User]] = ??? // Optional: Override for pagination/filtering support override def collection(isCount: Boolean, params: QueryParams)(implicit session: Session) = { // Use params.limit, params.offset, params.filters ??? } }import com.sapienapps.http4s.ErrorHandler case class MyErrorHandler[F[_]]() extends ErrorHandler[F, AppError] { def handle(e: AppError)(implicit m: Monad[F]): F[Response[F]] = e match { case NotFound(msg) => NotFound(msg) case ValidationError(msg) => BadRequest(msg) case _ => InternalServerError("Something went wrong") } }Open (unauthenticated) endpoints:
import com.sapienapps.http4s.open.UniversalEndpoint val endpoint = UniversalEndpoint[IO, String, User, AppError, String, Any, Session]( toParams = _ => Right(Map.empty), toSession = _ => Session(), errorHandler = MyErrorHandler[IO](), toId = identity ) // Using a service val routes: HttpRoutes[IO] = endpoint.endpoints(UniversalService(UserRepo[IO]())) // Or directly with a repository (convenience method) val routes: HttpRoutes[IO] = endpoint.endpoints(UserRepo[IO]())Authenticated endpoints:
import com.sapienapps.http4s.auth.AuthUniversalEndpoint import org.http4s.server.AuthMiddleware val authEndpoint = AuthUniversalEndpoint[IO, String, User, AppError, String, Any, UserContext]( toParams = _ => Right(Map.empty), toSession = (_, ctx) => ctx, errorHandler = MyErrorHandler[IO](), toId = identity ) // Using a service val authRoutes: HttpRoutes[IO] = authEndpoint.endpoints( UniversalService(UserRepo[IO]()), authMiddleware ) // Or directly with a repository val authRoutes: HttpRoutes[IO] = authEndpoint.endpoints(UserRepo[IO](), authMiddleware)Each endpoint class generates the following routes:
| Method | Path | Description | Query Params |
|---|---|---|---|
| POST | / | Create entity | - |
| GET | /:id | Get entity by ID | - |
| GET | / | List entities | limit, offset, custom filters |
| GET | /count | Get entity count | - |
| PUT | / | Update entity | - |
| DELETE | /:id | Delete entity by ID | - |
The list endpoint (GET /) automatically extracts query parameters:
GET /users?limit=10&offset=20&status=active&role=admin These are passed to your repository as QueryParams:
case class QueryParams( limit: Option[Int] = None, offset: Option[Int] = None, filters: Map[String, String] = Map.empty )CrudEndpoint (HTTP routing) ↓ CrudService (business logic) ← optional, can use repo directly ↓ CrudRepository (data access) - Scala 2.13 & Scala 3.3 LTS (cross-compiled)
- http4s 0.23.x (Ember)
- Circe (JSON)
- Cats Effect 3
MIT