Starting from scratch
We will walk you through how to set up a workspace and develop a Go application with Namespace. You can find a completed version at examples/golang/01-simple. If you want to try Namespace without any setup, we recommend exploring one of our examples.
Install the CLI tool
If you haven't installed the ns
command yet, refer to the Getting Started page.
Initialize your workspace
Namespace operates inside workspaces.
Each contains a ns-workspace.cue
file at its root which
- defines a module (a collection of Namespace packages).
- manages dependencies to other modules.
- defines development environments (e.g., dev, staging, prod).
Let's create a new workspace for our playground:
mkdir ns-example && cd ns-example
ns mod init example.com/ns-example
Setup your workstation for local development
Namespace manages your development environments, whether locally or remotely, in the cloud. For this example, we'll set up a local development environment, which includes our build infrastructure and Kubernetes, running within Docker (but don't worry, you don't need to manage it).
ns prepare local
ns
just wrote a devhost.textpb
file. This file configures your environments defined in ns-workspace.cue
.
Unlike ns-workspace.cue
, devhost.textpb
is typically not committed as the environment configuration can be machine-specific.
Your first Namespace application
We'll develop a simple Go application that uses MinIO to store data in a S3 bucket.
Build the skeleton
Let's start with a simple Go server and create a package (directory) server
.
First, we define the server in a server.cue
file:
// ns-example/server/server.cue
server: {
name: "go-server"
integration: "go"
services: {
webapi: {
port: 4000
kind: "http"
ingress: true
}
}
}
Now, we can implement the code skeleton in main.go
:
// ns-example/server/main.go
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
)
const httpPort = 4000 // Could be read from /namespace/config/runtime.json.
func main() {
r := mux.NewRouter()
r.PathPrefix("/").HandlerFunc(
func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(200)
fmt.Fprintln(rw, "Hello, world!")
})
log.Printf("Listening on port: %d\n", httpPort)
http.ListenAndServe(fmt.Sprintf(":%d", httpPort), r)
}
And finally, in our workspace root:
go mod init example.com/ns-example && go mod tidy
Start a development session
With the basic skeleton, we can start our server for the first time.
ns dev server
The argument server
refers to the package containing our Go server.
When accessing http://webapi-qqu16q45ib830tir.dev.nslocal.host:40080/
, we can see our Hello, world!
message. Yay!
What happened under the hood?
From a single command, Namespace performed a lot of work for us:
- set up a code watch to react to code changes
- build our Go server with best-in-class caching
- create the necessary Kubernetes resources: a deployment, a service, an ingress
- stream server logs into your console
- port-forward
webapi
service from Kubernetes to your local machine vianslocal.host
- start a development UI server allowing you to explore your deployment

You can keep ns dev
running while you develop the application.
It will react to your code changes and rebuild/redeploy servers as needed.
Define MinIO server
In this guide we'll walk you through how to setup a MinIO server from scratch. But most of the time you'll want to use an existing definition. For example, you can find a reusable version of MinIO at library.namespace.so/oss/minio/server.
We'll create another server.cue
file, this time in a minio
package:
// ns-example/minio/server.cue
server: {
name: "my-minio-server"
image: "minio/minio@sha256:de46799fc1ced82b784554ba4602b677a71966148b77f5028132fc50adf37b1f"
class: "stateful"
env: {
MINIO_ROOT_USER: "TestOnlyUser"
MINIO_ROOT_PASSWORD: "TestOnlyPassword"
}
args: [
"server",
"/minio",
"--address=:9000",
"--console-address=:9001",
]
services: {
api: {
port: 9000
kind: "http"
}
console: {
port: 9001
kind: "http"
}
}
mounts: {
"/minio": persistent: {
id: "my-minio-server-data"
size: "10GiB"
}
}
}
We configured MinIO to use a persistent volume to store its data and exposed its api
and console
services.
For simplicity, we're using hard-coded credentials.
See examples/golang/02-withsecrets for an example that relies on generated secrets.
Connect your servers
It's time to make our application server call the freshly defined object-store.
// Updates to ns-example/server/server.cue
server: {
name: "go-server"
integration: "go"
requires: [
"example.com/ns-example/minio",
]
env: {
S3_ENDPOINT: fromServiceEndpoint: "example.com/ns-example/minio:api"
}
services: {
webapi: {
port: 4000
kind: "http"
ingress: true
}
}
}
The requires
block informs Namespace which servers are direct dependencies.
When deploying your application, Namespace will
- calculate a complete dependency graph
- translate the graph into required Kubernetes operations
- sort the operations topologically
- execute the operations in order while guaranteeing that each server's dependencies are ready before deploying itself
Since ns dev
is still running, all of this has already happened, and Namespace brought up MinIO for you.

For convenience, we also ask Namespace to inject the service endpoint of our dependency into an environment variable. Time to wire it up!
Final application
Given the injected endpoint, we can now connect to the data store, create a bucket and implement handlers that use it.
// ns-example/server/main.go
package main
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"os"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/gorilla/mux"
)
const (
accessKeyID = "TestOnlyUser"
secretAccessKey = "TestOnlyPassword"
bucketName = "test-bucket"
httpPort = 4000 // Could be read from /namespace/config/runtime.json.
)
func main() {
ctx := context.Background()
resolver := func(_, region string, _ ...interface{}) (aws.Endpoint, error) {
return aws.Endpoint{
PartitionID: "aws",
URL: fmt.Sprintf("http://%s", os.Getenv("S3_ENDPOINT")),
SigningRegion: region,
}, nil
}
cfg, err := config.LoadDefaultConfig(ctx,
config.WithEndpointResolverWithOptions(
aws.EndpointResolverWithOptionsFunc(resolver)),
config.WithCredentialsProvider(
credentials.NewStaticCredentialsProvider(
accessKeyID, secretAccessKey, "" /* session */)))
if err != nil {
log.Fatal(err)
}
cli := s3.NewFromConfig(cfg, func(o *s3.Options) { o.UsePathStyle = true })
if _, err = cli.CreateBucket(ctx, &s3.CreateBucketInput{
Bucket: aws.String(bucketName),
}); err != nil {
var alreadyExists *types.BucketAlreadyExists
var alreadyOwned *types.BucketAlreadyOwnedByYou
if !errors.As(err, &alreadyExists) && !errors.As(err, &alreadyOwned) {
log.Fatal(err)
}
}
r := mux.NewRouter()
r.HandleFunc("/put", put(cli))
r.HandleFunc("/get", get(cli))
r.PathPrefix("/").HandlerFunc(
func(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(200)
fmt.Fprintln(rw, "Hello, world!")
})
log.Printf("Listening on port: %d\n", httpPort)
http.ListenAndServe(fmt.Sprintf(":%d", httpPort), r)
}
type PutRequest struct {
Key string `json:"key"`
Body []byte `json:"body"`
}
type GetRequest struct {
Key string `json:"key"`
}
type GetResponse struct {
Body []byte `json:"body"`
}
func put(cli *s3.Client) func(http.ResponseWriter, *http.Request) {
return func(rw http.ResponseWriter, req *http.Request) {
var parsed PutRequest
if err := json.NewDecoder(req.Body).Decode(&parsed); err != nil {
rw.WriteHeader(400)
fmt.Fprintf(rw, "invalid request: %v\n", err)
return
}
if _, err := cli.PutObject(req.Context(), &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(parsed.Key),
Body: bytes.NewReader(parsed.Body),
}); err != nil {
rw.WriteHeader(500)
fmt.Fprintf(rw, "failed to upload object: %v\n", err)
return
}
}
}
func get(cli *s3.Client) func(http.ResponseWriter, *http.Request) {
return func(rw http.ResponseWriter, req *http.Request) {
var parsed GetRequest
if err := json.NewDecoder(req.Body).Decode(&parsed); err != nil {
rw.WriteHeader(400)
fmt.Fprintf(rw, "invalid request: %v\n", err)
return
}
out, err := cli.GetObject(req.Context(), &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(parsed.Key),
})
if err != nil {
rw.WriteHeader(500)
fmt.Fprintf(rw, "failed to get object: %v\n", err)
return
}
content, err := io.ReadAll(out.Body)
if err != nil {
rw.WriteHeader(500)
fmt.Fprintf(rw, "failed to read object: %v\n", err)
return
}
resp := GetResponse{
Body: content,
}
serialized, err := json.Marshal(resp)
if err != nil {
rw.WriteHeader(500)
fmt.Fprintf(rw, "internal error: %v\n", err)
return
}
fmt.Fprintln(rw, string(serialized))
}
}
And finally, update your go.mod
file to include the new dependencies:
# In the workspace root.
go mod tidy
🎉 You did it!
You just built your first application with Namespace!
What's next?
- Explore Namespace's documentation
- Check out our examples
- Chat with the team on Discord
- And star Namespace on Github