Project Structure
When you create a new Tram application, you'll be greeted with a starter application. There's a lot to explore here, so let's quickly explore the different areas of the starter as an introduction.
The file structure of your project starts something like this.
sample-app
├── bin
│ ├── db-init
│ └── test
├── build.clj
├── deps.edn
├── dev
│ ├── migrations.clj
│ ├── runtimes
│ └── user.clj
├── docker-compose.yml
├── e2e
├── mise.toml
├── README.md
├── resources
│ ├── migrations
│ ├── seeds
│ └── tailwindcss
├── src
│ └── sample_app
│ ├── concerns
│ ├── config.clj
│ ├── core.clj
│ ├── db.clj
│ ├── handlers
│ ├── models
│ ├── routes.clj
│ ├── server.clj
│ └── views
├── tasks
│ └── dev
│ ├── server.clj
│ └── tailwind.clj
├── tests.edn
└── tram.ednDevelopment time code dev/
Because you'll interact with the REPL so much, you'll likely want places to put code that is meant for interaction with the REPL. That can live in /dev.
dev/migrations.clj, for example, is a namespace that you can use to run migrations without starting a new JVM.
Build file build.clj
This file is a build script for your application. You can run Tram apps via clj or you can compile them to a .jar file and run them as a plain JVM app.
TIP
Compile your application with clj -T:build uber
Handlers src/sample_app/handlers
Handlers are functions that take a request map and return a response map.
Important
Handlers must live in a file like user_handlers.clj for automagic in Tram to work properly.
The domain prefix should be a singular noun.
These functions also export one vector, typically called routes, defined with tram.routes/defroutes. These are a segment your application routes. Routes are embedded in another routes vector up to the root routes collection in src/sample_app/routes.clj where the route data is passed to tram.routes/tram-router.
Typically, you'll interact with keys like these in a request:
{:parameters {:path {:user-id 2}
:body {:name "Fred"}}
:current-user {:id 3
:name "Bob"}}There are likely going to be a lot of big maps in the request map because injection is a key way of passing data around in Tram.
A typical repsonse might look like
{:status 200
:locals {:user user}}Read more about routes in the routing guide.
Views src/sample_app/views
Views are typically a 1:1 mapping of handlers. Via naming conventiosn you can read about in the in the routing guide, views are automatically looked up for corresponding handlers.
In Tram, a view is simply a function that receives a single argument, the locals map from a handler, and returns a vector of hiccup (html).
Read more about views in the views guide.
Models src/sample_app/models
Models in Tram are dramatically lighter than in other frameworks. Their primary concern is interacting with the database. These namespaces are not to define functions that operate on your model, but to add custom behavior and queries to simplify database interaction.
For example this snippet which will remove the password key from a user model after being retrieved from the database so you don't accidentally select it.
(db/define-after-select :models/users
[user]
(dissoc user :password))Read more about model files in the model-guide.
Concerns src/sample_app/concerns
Concerns are domain areas where most of your logic should live. These can be split up in any way that makes sense for your application.
Read more about concerns in the concern guide.
Components src/sample_app/components
Components are reusable view functions that return hiccup (html).
Read more in the components guide.
Application system src/sample_app/config.clj
Tram uses Integrant to connect various stateful parts of your application. These are defined here in a system configuration map, and they are started in order based on a dependency graph.
Until you need to add new system components (something like Redis), you likely won't need to touch this.
Application Entry src/sample_app/core.clj
Root to your application. This sets up the server, initializes the system configuration, and boots up the application.
Database Configuration `src/sample_app/db.
This is where you'll write any global configuration for your database.
By default, the only thing this file does is set a default connection to your database based on env.f
Root Routes and Router src/sample_app/routes.clj
You'll need to add new routes and interceptors here.
Http Server src/sample_app/server.clj
Starts the http server.
bin
For external binaries your application might need. Scripts that are external to Tram. By default there is a test binary, which runs your tests with Kaocha, and there is a init-db script to create your databases.
deps.edn
Root Clojure configuration file. Think of a package.json or Gemfile.
e2e
Holds a npm project that can be used to test e2e with Playwright. For now, this is the recommended approach. There is some scaffolding there to make it easy to get started.
mise.toml
Environment, tool, and task configuration.
Read their official docs for more information.
resources
This holds non source code requirements for your application. These are included on the classpath by default.
Included are an images directory (not images served on your server); a migrations dir for sql migrations; a public dir for assets accessible over http; a seeds dir, for Clojure code that initializes seed data; and a tailwindcss project for generating index.css from tailwind classes that you use in your views.
test
Directory for tests.
