Haskell codebase containing real world examples (CRUD, auth, advanced patterns, etc) that adheres to the RealWorld spec and API.
This codebase was created to demonstrate a fully fledged fullstack application built with Haskell/Scotty including CRUD operations, authentication, routing, pagination, and more.
We've gone to great lengths to adhere to the Haskell/Scotty community styleguides & best practices.
For more information on how to this works with other frontends/backends, head over to the RealWorld repo.
Since this is small application (only 2k+ϵ actual LOC), I've opted for a very vertical-slice-esque architecture, with each endpoint getting its own file, and with common logic simply going in its own files. Controllers, services, and data access are of course still decoupled through MTL style classes (inspired by three-layer-cake), making testing extremely simple. I believe the general architecture should be quite self explanitory and easy to refactor as the app scales. I will admit it is slightly influenced by Haskell's dissallowance of circular imports, and I couldn't get hls to work with an hs-boot file.
Feel free to let me know your thoughts, or open an issue!
View more fine-grained documentation here.
(Not including common dependencies such as mtl
and aeson
)
scotty
— The minimal web "framework" which makes this all possiblerelude
— A nicer/safer Prelude alternativeesqueleto
— A type-safe SQL eDSL wrappingpersistent
jwt
— Library for working w/ JWTscryptonite
— Low-level cryptography librarywai-middleware-static
— Used to easily serve static filesfile-embed
— Used to embed the sqlbits directly into haskell code
app/ # The entrypoint into the application.
Main.hs # Very thin, just deals with configuration.
sqlbits/ # Some sql files w/ triggers/functions
# which are embedded directly within
# the haskell files via TemplateHaskell.
src/ # The actual source code for Conduit.
Conduit/App/ # This folder deals with the App Monad,
# which holds the server's global state.
Conduit/DB/ # Holds some DB-related utilities and
# code for decoupling/abstraction purposes.
# Also holds some DB initializtion code.
Conduit/Features/ # Contains the bulk of the Conduit logic,
# including the API endpoints/services/DB
# access logic.
# Also holds feature-related DB/error logic.
Conduit/Identity/ # Holds the code for the JWT-based auth.
Errors.hs # Some code for handling and translating
# feature-specific errors
Validation.hs # Some utilities for basic validation
# of incoming data.
static/ # Holds the static files where the avatar
# images reside. The images are actually just
# blank files for now but it's fiiine.
test/ # Contains the unit tests made w/ hspec,
# currently only have tests for auth and
# slug-building since cypress covers the rest.
package.yaml # Describes the project and its dependencies.
realworld-hs.cabal # hpack generates the .cabal file from the
# package.yaml which, IMO, is much nicer
# to work with.
conduit-schema.json # The configuration files for conduit itself.
conduit.json # The schema file for your convenience.
On ExceptT vs Exceptions: yeah uh, besides the lack of structural typing, from my non-expert opinion, one of my biggest issues with Haskell is the lack of truly idiomatic error handling; ask 10 different people, get 11 different answers. I ended up going with ExceptT because it seemed like the simplest method while maintaining sufficient cleanliness and typechecking.
On not just writing into the AppM monad: I easily could've used (AppM m)
as a constraint directly, everywhere. And some people would argue that I
should've, on an app on this scale—which is totally fair: it's much simpler and lightweight, compared to the (somewhat?) three-layer-style I opted for.
However, I do find semantic meaning in seeing some context such as (PasswordGen m, AuthTokenGen m, CreateUser m, ReadUsers m)
where it's immediately
clear what the function needs access to. It's also arguably better for testing, but I didn't really test the services here (due to the tests provided
by gothinkster), so I won't go into that here. It's arguable that such contexts are merely implementation details, but I'd say that it's more than
a detail; it's a purpose, the essence of that function. But that's just me, do whatever you want lol.
This project requires Cabal & a running Postgres instance. Here're a couple links of that may help you out:
- Cabal via GHCup — Guide for installing + getting started w/ Cabal
- Postgres + Docker — A quick tutorial for getting a Postgres container running
To begin, of course, clone the repo and cd into it:
git clone https://github.com/toptobes/realworld-scotty-hs.git && cd realworld-scotty-hs
# or
gh repo clone toptobes/realworld-scotty-hs && cd realworld-scotty-hs
From there, update the appropriate configs in conduit.json
- It's already set with sensible defaults; be sure to double-check the postgres connection string
- there is a
conduit-schema.json
for your convenience as well that provides extra info - If necessary, you can set the
CONDUIT_CONFIG
env var to to some custom config path
Then, you can start the app via cabal run app
, or run the unit tests w/ cabal run spec
.
It may take a while the first time while it obtains/builds the relevant dependencies.
If you plan to make any modifications involving creating new files or adding new dependencies, you may need hpack.
- You could update the
realworld-hs.cabal
file manually if you really wanted or needed to though - Otherwise, just run the
hpack
command whenever you modifypackage.yaml
Access the documentation site @ [https://toptobes.github.io/realworld-scotty-hs/]. Nearly everything except for the features are decently documented, and I'll work on adding more soon.
You can also build the documentation site locally using cabal haddock
, and then serve it w/ npx serve
or whatever
else you fancy.
Or just read through the code manually if you prefer, whatever you want lol.